wangshan 5 달 전
부모
커밋
fc7c805046

+ 0 - 108
captcha/captcha.go

@@ -1,108 +0,0 @@
-package captcha
-
-import (
-	"app.yhyue.com/moapp/jybase/go-xweb/httpsession"
-	"app.yhyue.com/moapp/jybase/redis"
-	"fmt"
-	"image/color"
-	"log"
-	"net/http"
-
-	"math/rand"
-
-	"github.com/lifei6671/gocaptcha"
-)
-
-// 图形验证码大小
-const (
-	dx = 90
-	dy = 30
-)
-
-// 生成的字符集
-var TextCharacters = []rune("ACDEFGHJKLMNPQRSTUVWXY2456789")
-
-// 初始化字体文件
-func InitCaptcha() {
-	err := gocaptcha.SetFontPath("./fonts/")
-	if err != nil {
-		log.Println(err)
-	}
-}
-
-// RandText 生成随机字体.
-func RandText(num int) string {
-	textNum := len(TextCharacters)
-	text := make([]rune, num)
-	for i := 0; i < num; i++ {
-		text[i] = TextCharacters[rand.Intn(textNum)]
-	}
-	return string(text)
-}
-
-// RandLightColor 随机生成浅色底色.
-func RandLightColor(n int) color.RGBA {
-	// 为每个颜色分量生成一个128到255之间的随机数
-	red := rand.Intn(100/n) + 156 + (100 - 100/n)
-	green := rand.Intn(100/n) + 156 + (100 - 100/n)
-	blue := rand.Intn(100/n) + 156 + (100 - 100/n)
-	// Alpha 通道设置为完全不透明
-	a := uint8(rand.Intn(8) + 247)
-
-	return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: a}
-}
-
-func Get(sess *httpsession.Session, w http.ResponseWriter, r *http.Request) {
-	str := RandText(4)
-	log.Println(str)
-	cacheKey := fmt.Sprintf("captcha_times_%s", sess.Id())
-	var (
-		// 计次数
-		times = redis.GetInt("other", cacheKey)
-		ttl   = redis.GetTTL("other", cacheKey)
-		//跳色
-		cc = RandLightColor(times%6 + 1)
-	)
-	// cc.A = uint8(rand.Intn(10) + 245)
-	captchaImage := gocaptcha.New(dx, dy, cc).
-		DrawBorder(gocaptcha.RandDeepColor())
-	if times < 3 {
-		captchaImage = captchaImage.DrawNoise(gocaptcha.NoiseDensityLower, gocaptcha.NewPointNoiseDrawer()).
-			DrawNoise(gocaptcha.NoiseDensityHigh, gocaptcha.NewTextNoiseDrawer(42)).
-			DrawLine(gocaptcha.NewBeeline(), gocaptcha.RandDeepColor()).
-			DrawText(gocaptcha.NewTwistTextDrawer(63, 15, 0.04), str)
-	} else if times < 6 {
-		captchaImage = captchaImage.DrawNoise(gocaptcha.NoiseDensityLower, gocaptcha.NewPointNoiseDrawer()).
-			DrawNoise(gocaptcha.NoiseDensityHigh, gocaptcha.NewTextNoiseDrawer(43)).
-			DrawLine(gocaptcha.NewBeeline(), gocaptcha.RandDeepColor()).
-			DrawText(gocaptcha.NewTwistTextDrawer(65, 14, 0.03), str).
-			DrawBlur(gocaptcha.NewGaussianBlur(), 2, 0.15)
-	} else if times < 10 {
-		captchaImage = captchaImage.DrawNoise(gocaptcha.NoiseDensityLower, gocaptcha.NewPointNoiseDrawer()).
-			DrawNoise(gocaptcha.NoiseDensityHigh, gocaptcha.NewTextNoiseDrawer(45)).
-			DrawLine(gocaptcha.NewBeeline(), gocaptcha.RandDeepColor()).
-			DrawText(gocaptcha.NewTwistTextDrawer(66, 18, 0.04), str).
-			DrawLine(gocaptcha.NewBeeline(), gocaptcha.RandDeepColor()).
-			DrawBlur(gocaptcha.NewGaussianBlur(), 2, 0.41)
-
-	} else {
-		captchaImage = captchaImage.DrawNoise(gocaptcha.NoiseDensityHigh, gocaptcha.NewTextNoiseDrawer(50)).
-			DrawNoise(gocaptcha.NoiseDensityLower, gocaptcha.NewPointNoiseDrawer()).
-			DrawLine(gocaptcha.NewBezier3DLine(), gocaptcha.RandLightColor()).
-			DrawLine(gocaptcha.NewBeeline(), gocaptcha.RandDeepColor()).
-			DrawText(gocaptcha.NewTwistTextDrawer(66, 18, 0.04), str).
-			DrawBlur(gocaptcha.NewGaussianBlur(), 2, 0.44)
-	}
-	if times == 0 {
-		ttl = 24 * 60 * 60
-	}
-	//计次
-	times++
-	redis.Put("other", cacheKey, times, int(ttl))
-	if captchaImage.Error != nil {
-		log.Println(captchaImage.Error)
-	}
-
-	_ = captchaImage.Encode(w, gocaptcha.ImageFormatJpeg)
-
-}

BIN
captcha/fonts/STCAIYUN.TTF


+ 12 - 41
go.mod

@@ -1,6 +1,6 @@
 module app.yhyue.com/moapp/jybase
 
-go 1.21
+go 1.13
 
 require (
 	app.yhyue.com/moapp/esv1 v0.0.0-20220414031211-3da4123e648d
@@ -11,56 +11,27 @@ require (
 	github.com/garyburd/redigo v1.6.2
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/golang-jwt/jwt/v4 v4.4.2
+	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
+	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gomodule/redigo v1.8.9
 	github.com/howeyc/fsnotify v0.9.0
-	github.com/lifei6671/gocaptcha v1.0.1
+	github.com/kr/text v0.2.0 // indirect
 	github.com/olivere/elastic/v7 v7.0.22
+	github.com/stretchr/testify v1.8.0 // indirect
 	github.com/yl2chen/cidranger v1.0.2
 	go.etcd.io/etcd/client/v3 v3.5.4
 	go.mongodb.org/mongo-driver v1.9.1
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/goleak v1.1.12 // indirect
+	go.uber.org/multierr v1.8.0 // indirect
 	go.uber.org/zap v1.21.0
+	golang.org/x/image v0.24.0
+	google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
+	google.golang.org/grpc v1.47.0 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc
+	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gorm.io/driver/mysql v1.0.5
 	gorm.io/gorm v1.21.3
 )
-
-require (
-	github.com/bits-and-blooms/bitset v1.2.0 // indirect
-	github.com/coreos/go-semver v0.3.0 // indirect
-	github.com/coreos/go-systemd/v22 v22.3.2 // indirect
-	github.com/go-stack/stack v1.8.0 // indirect
-	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/golang/snappy v0.0.4 // indirect
-	github.com/google/go-cmp v0.5.8 // indirect
-	github.com/jinzhu/inflection v1.0.0 // indirect
-	github.com/jinzhu/now v1.1.1 // indirect
-	github.com/josharian/intern v1.0.0 // indirect
-	github.com/klauspost/compress v1.13.6 // indirect
-	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/mschoch/smat v0.2.0 // indirect
-	github.com/olivere/elastic v6.2.37+incompatible // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/stretchr/testify v1.8.0 // indirect
-	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
-	github.com/xdg-go/scram v1.0.2 // indirect
-	github.com/xdg-go/stringprep v1.0.2 // indirect
-	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
-	go.etcd.io/etcd/api/v3 v3.5.4 // indirect
-	go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
-	go.uber.org/atomic v1.9.0 // indirect
-	go.uber.org/goleak v1.1.12 // indirect
-	go.uber.org/multierr v1.8.0 // indirect
-	golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 // indirect
-	golang.org/x/image v0.22.0 // indirect
-	golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect
-	golang.org/x/sync v0.9.0 // indirect
-	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
-	golang.org/x/text v0.20.0 // indirect
-	google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
-	google.golang.org/grpc v1.47.0 // indirect
-	google.golang.org/protobuf v1.28.0 // indirect
-)

+ 67 - 15
go.sum

@@ -34,6 +34,7 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coscms/tagfast v0.0.0-20150925144250-2b69b2496250 h1:svuVnTdfOrAZFzcdpau9u91veXKEdpHDNpMzb132vPs=
 github.com/coscms/tagfast v0.0.0-20150925144250-2b69b2496250/go.mod h1:zX8vynptAghuV/KG8BOZlDeo4DsTKWfBQ154RWlkay0=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -106,8 +107,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -136,10 +137,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lifei6671/gocaptcha v1.0.1 h1:GWQp/OCEpMg+OIiNA1BBVdP7AAls98hfCYoSolJWBT8=
-github.com/lifei6671/gocaptcha v1.0.1/go.mod h1:Nus08m55YFlZve/ZDUyzFbJot0FR0R3OuAAX4WF2vic=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -213,6 +216,7 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
 go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
 go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
@@ -240,20 +244,30 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8=
-golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
-golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
+golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
+golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -268,9 +282,15 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
-golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -283,8 +303,13 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
-golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -306,16 +331,36 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
-golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -328,6 +373,11 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -373,6 +423,8 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=

+ 26 - 0
gocaptcha/.gitignore

@@ -0,0 +1,26 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+.idea
+.git
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof

+ 201 - 0
gocaptcha/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 68 - 0
gocaptcha/README.md

@@ -0,0 +1,68 @@
+# gocaptcha
+一个简单的Go语言实现的验证码
+
+### 图片实例
+
+![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_1.jpg)
+![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_2.jpg)
+![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_3.jpg)
+![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_4.jpg)
+
+## 简介
+
+基于Golang实现的图片验证码生成库,可以实现随机字母个数,随机直线,随机噪点等。可以设置任意多字体,每个验证码随机选一种字体展示。
+
+## 实例
+
+#### 使用:
+
+```
+	go get github.com/lifei6671/gocaptcha
+```
+
+#### 使用的类库
+
+```
+	go get github.com/golang/freetype
+	go get github.com/golang/freetype/truetype
+	go get golang.org/x/image
+```
+
+#### 代码
+具体实例可以查看example目录,有生成的验证码图片。
+
+```
+	
+func Get(w http.ResponseWriter, r *http.Request) {
+	captchaImage := gocaptcha.New(dx, dy, gocaptcha.RandLightColor())
+	err := captchaImage.
+		DrawBorder(gocaptcha.RandDeepColor()).
+		DrawNoise(gocaptcha.NoiseDensityHigh, gocaptcha.NewTextNoiseDrawer(gocaptcha.DefaultDPI)).
+		DrawNoise(gocaptcha.NoiseDensityLower, gocaptcha.NewPointNoiseDrawer()).
+		DrawLine(gocaptcha.NewBezier3DLine(), gocaptcha.RandDeepColor()).
+		DrawText(gocaptcha.NewTwistTextDrawer(gocaptcha.DefaultDPI, gocaptcha.DefaultAmplitude, gocaptcha.DefaultFrequency), gocaptcha.RandText(4)).
+		DrawLine(gocaptcha.NewBeeline(), gocaptcha.RandDeepColor()).
+		//DrawLine(gocaptcha.NewHollowLine(), gocaptcha.RandLightColor()).
+		DrawBlur(gocaptcha.NewGaussianBlur(), gocaptcha.DefaultBlurKernelSize, gocaptcha.DefaultBlurSigma).
+		Error
+	
+	if err != nil {
+		fmt.Println(err)
+	}
+	
+	_ = captchaImage.Encode(w, gocaptcha.ImageFormatJpeg)
+}
+
+// 初始化字体
+func init() {
+	err := gocaptcha.SetFontPath("../fonts/")
+	if err != nil {
+		panic(err)
+	}
+}
+
+```
+
+
+
+

+ 89 - 0
gocaptcha/blur.go

@@ -0,0 +1,89 @@
+package gocaptcha
+
+import (
+	"image/color"
+	"image/draw"
+	"math"
+)
+
+type BlurDrawer interface {
+	DrawBlur(canvas draw.Image, kernelSize int, sigma float64) error
+}
+
+type gaussianBlur struct {
+}
+
+func NewGaussianBlur() BlurDrawer {
+	return &gaussianBlur{}
+}
+
+func (g *gaussianBlur) DrawBlur(canvas draw.Image, kernelSize int, sigma float64) error {
+	kernel := g.generateGaussianKernel(kernelSize, sigma)
+	bounds := canvas.Bounds()
+
+	for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
+		for x := bounds.Min.X; x < bounds.Max.X; x++ {
+			r, g, b := g.applyKernel(canvas, x, y, kernel)
+			canvas.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255})
+		}
+	}
+	return nil
+}
+
+func (g *gaussianBlur) generateGaussianKernel(kernelSize int, sigma float64) [][]float64 {
+	kernel := make([][]float64, kernelSize)
+	sum := 0.0
+	mid := kernelSize / 2
+
+	for i := 0; i < kernelSize; i++ {
+		kernel[i] = make([]float64, kernelSize)
+		for j := 0; j < kernelSize; j++ {
+			x := float64(i - mid)
+			y := float64(j - mid)
+			kernel[i][j] = math.Exp(-(x*x+y*y)/(2*sigma*sigma)) / (2 * math.Pi * sigma * sigma)
+			sum += kernel[i][j]
+		}
+	}
+
+	// Normalize kernel
+	for i := 0; i < kernelSize; i++ {
+		for j := 0; j < kernelSize; j++ {
+			kernel[i][j] /= sum
+		}
+	}
+
+	return kernel
+}
+
+func (g *gaussianBlur) applyKernel(canvas draw.Image, x int, y int, kernel [][]float64) (uint8, uint8, uint8) {
+	bounds := canvas.Bounds()
+	size := len(kernel)
+	mid := size / 2
+	var r1, g1, b1 float64
+
+	for ky := 0; ky < size; ky++ {
+		for kx := 0; kx < size; kx++ {
+			px := x + kx - mid
+			py := y + ky - mid
+			if px >= bounds.Min.X && px < bounds.Max.X && py >= bounds.Min.Y && py < bounds.Max.Y {
+				rr, gg, bb, _ := canvas.At(px, py).RGBA()
+				k := kernel[ky][kx]
+				r1 += k * float64(rr>>8)
+				g1 += k * float64(gg>>8)
+				b1 += k * float64(bb>>8)
+			}
+		}
+	}
+
+	return g.clamp(r1), g.clamp(g1), g.clamp(b1)
+}
+
+func (g *gaussianBlur) clamp(value float64) uint8 {
+	if value < 0 {
+		return 0
+	}
+	if value > 255 {
+		return 255
+	}
+	return uint8(value)
+}

+ 39 - 0
gocaptcha/blur_test.go

@@ -0,0 +1,39 @@
+package gocaptcha
+
+import (
+	"image"
+	"image/draw"
+	"testing"
+)
+
+func TestDrawBlur(t *testing.T) {
+	type args struct {
+		canvas     draw.Image
+		kernelSize int
+		sigma      float64
+	}
+	tests := []struct {
+		name    string
+		t       BlurDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    NewGaussianBlur(),
+			args: args{
+				canvas:     image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				kernelSize: 5,
+				sigma:      1.0,
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawBlur(tt.args.canvas, tt.args.kernelSize, tt.args.sigma); (err != nil) != tt.wantErr {
+				t.Errorf("textDrawer.DrawString() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 129 - 0
gocaptcha/captcha.go

@@ -0,0 +1,129 @@
+package gocaptcha
+
+import (
+	"errors"
+	"image"
+	"image/color"
+	"image/draw"
+	"image/gif"
+	"image/jpeg"
+	"image/png"
+	"io"
+	"math/rand"
+)
+
+const (
+	// DefaultDPI 默认的dpi
+	DefaultDPI = 72.0
+	// DefaultBlurKernelSize 默认模糊卷积核大小
+	DefaultBlurKernelSize = 2
+	// DefaultBlurSigma 默认模糊sigma值
+	DefaultBlurSigma = 0.65
+	// DefaultAmplitude 默认图片扭曲的振幅
+	DefaultAmplitude = 20
+	//DefaultFrequency 默认图片扭曲的波频率
+	DefaultFrequency = 0.05
+)
+
+//var TextCharacters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
+
+const (
+	ImageFormatPng ImageFormat = iota
+	ImageFormatJpeg
+	ImageFormatGif
+)
+
+// ImageFormat 图片格式
+type ImageFormat int
+
+type CaptchaImage struct {
+	nrgba   *image.NRGBA
+	width   int
+	height  int
+	Complex int
+	Error   error
+}
+
+// New 新建一个图片对象
+func New(width int, height int, bgColor color.RGBA) *CaptchaImage {
+	m := image.NewNRGBA(image.Rect(0, 0, width, height))
+
+	draw.Draw(m, m.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src)
+
+	return &CaptchaImage{
+		nrgba:  m,
+		height: height,
+		width:  width,
+	}
+}
+
+// Encode 编码图片
+func (captcha *CaptchaImage) Encode(w io.Writer, imageFormat ImageFormat) error {
+
+	if imageFormat == ImageFormatPng {
+		return png.Encode(w, captcha.nrgba)
+	}
+	if imageFormat == ImageFormatJpeg {
+		return jpeg.Encode(w, captcha.nrgba, &jpeg.Options{Quality: 100})
+	}
+	if imageFormat == ImageFormatGif {
+		return gif.Encode(w, captcha.nrgba, &gif.Options{NumColors: 256})
+	}
+
+	return errors.New("not supported image format")
+}
+
+// DrawLine 画直线.
+func (captcha *CaptchaImage) DrawLine(drawer LineDrawer, lineColor color.Color) *CaptchaImage {
+	if captcha.Error != nil {
+		return captcha
+	}
+	y := captcha.nrgba.Bounds().Dy()
+	point1 := image.Point{X: captcha.nrgba.Bounds().Min.X + 1, Y: rand.Intn(y)}
+	point2 := image.Point{X: captcha.nrgba.Bounds().Max.X - 1, Y: rand.Intn(y)}
+	captcha.Error = drawer.DrawLine(captcha.nrgba, point1, point2, lineColor)
+	return captcha
+}
+
+// DrawBorder 画边框.
+func (captcha *CaptchaImage) DrawBorder(borderColor color.RGBA) *CaptchaImage {
+	if captcha.Error != nil {
+		return captcha
+	}
+	for x := 0; x < captcha.width; x++ {
+		captcha.nrgba.Set(x, 0, borderColor)
+		captcha.nrgba.Set(x, captcha.height-1, borderColor)
+	}
+	for y := 0; y < captcha.height; y++ {
+		captcha.nrgba.Set(0, y, borderColor)
+		captcha.nrgba.Set(captcha.width-1, y, borderColor)
+	}
+	return captcha
+}
+
+// DrawNoise 画噪点.
+func (captcha *CaptchaImage) DrawNoise(complex NoiseDensity, noiseDrawer NoiseDrawer, rn int) *CaptchaImage {
+	if captcha.Error != nil {
+		return captcha
+	}
+	captcha.Error = noiseDrawer.DrawNoise(captcha.nrgba, complex, rn)
+	return captcha
+}
+
+// DrawText 写字.
+func (captcha *CaptchaImage) DrawText(textDrawer TextDrawer, text string) *CaptchaImage {
+	if captcha.Error != nil {
+		return captcha
+	}
+	captcha.Error = textDrawer.DrawString(captcha.nrgba, text)
+	return captcha
+}
+
+// DrawBlur 对图片进行模糊处理
+func (captcha *CaptchaImage) DrawBlur(drawer BlurDrawer, kernelSize int, sigma float64) *CaptchaImage {
+	if captcha.Error != nil {
+		return captcha
+	}
+	captcha.Error = drawer.DrawBlur(captcha.nrgba, kernelSize, sigma)
+	return captcha
+}

+ 24 - 0
gocaptcha/captcha_test.go

@@ -0,0 +1,24 @@
+package gocaptcha
+
+import "testing"
+
+func TestCaptchaImage_Encode(t *testing.T) {
+	err := SetFontPath("./fonts")
+	if err != nil {
+		t.Fatal(err)
+	}
+	captchaImage := New(150, 20, RandLightColor(Rn))
+	err = captchaImage.
+		DrawBorder(RandDeepColor()).
+		DrawNoise(NoiseDensityHigh, NewTextNoiseDrawer(72), Rn).
+		DrawNoise(NoiseDensityLower, NewPointNoiseDrawer(), Rn).
+		DrawLine(NewBezier3DLine(), RandDeepColor()).
+		DrawText(NewTwistTextDrawer(DefaultDPI, DefaultAmplitude, DefaultFrequency), RandText(4)).
+		DrawLine(NewBeeline(), RandDeepColor()).
+		DrawLine(NewHollowLine(), RandLightColor(Rn)).
+		DrawBlur(NewGaussianBlur(), DefaultBlurKernelSize, DefaultBlurSigma).
+		Error
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 101 - 0
gocaptcha/font.go

@@ -0,0 +1,101 @@
+package gocaptcha
+
+import (
+	"math/rand"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+
+	"github.com/golang/freetype"
+	"github.com/golang/freetype/truetype"
+)
+
+var DefaultFontFamily = NewFontFamily()
+var ErrNoFontsInFamily = os.ErrNotExist
+
+// SetFonts sets the default font family
+func SetFonts(fonts ...string) error {
+	for _, font := range fonts {
+		if err := DefaultFontFamily.AddFont(font); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// SetFontPath sets the default font family from a directory
+func SetFontPath(fontDirPath string) error {
+	return DefaultFontFamily.AddFontPath(fontDirPath)
+}
+
+// FontFamily is a font family that creates a new font family
+type FontFamily struct {
+	fonts     []string
+	fontCache *sync.Map
+	r         *rand.Rand
+}
+
+// Random returns a random font from the family
+func (f *FontFamily) Random() (*truetype.Font, error) {
+	if len(f.fonts) == 0 {
+		return nil, ErrNoFontsInFamily
+	}
+	fontFile := f.fonts[f.r.Intn(len(f.fonts))]
+	if v, ok := f.fontCache.Load(fontFile); ok {
+		return v.(*truetype.Font), nil
+	}
+	font, err := f.parseFont(fontFile)
+	if err != nil {
+		return nil, err
+	}
+	f.fontCache.Store(fontFile, font)
+	return font, nil
+}
+
+func (f *FontFamily) parseFont(fontFile string) (*truetype.Font, error) {
+	fontBytes, err := os.ReadFile(fontFile)
+	if err != nil {
+		return nil, err
+	}
+	font, err := freetype.ParseFont(fontBytes)
+	if err != nil {
+		return nil, err
+	}
+	return font, nil
+}
+
+// AddFont adds a font to the family and returns an error if it fails
+func (f *FontFamily) AddFont(fontFile string) error {
+	if _, ok := f.fontCache.Load(fontFile); ok {
+		return nil
+	}
+	font, err := f.parseFont(fontFile)
+	if err != nil {
+		return err
+	}
+	f.fonts = append(f.fonts, fontFile)
+	f.fontCache.Store(fontFile, font)
+	return nil
+}
+
+// AddFontPath adds all .ttf files from the given directory to the font family and returns an error if any
+func (f *FontFamily) AddFontPath(dirPath string) error {
+	return filepath.Walk(dirPath, func(path string, info os.FileInfo, walkErr error) error {
+		if walkErr != nil {
+			return walkErr
+		}
+		if !info.IsDir() && filepath.Ext(path) == ".ttf" {
+			return f.AddFont(path)
+		}
+		return nil
+	})
+}
+
+// NewFontFamily creates a new font family with the given fonts
+func NewFontFamily() *FontFamily {
+	return &FontFamily{
+		fontCache: &sync.Map{},
+		r:         rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}

+ 157 - 0
gocaptcha/font_test.go

@@ -0,0 +1,157 @@
+package gocaptcha
+
+import (
+	"testing"
+)
+
+func TestFontFamily_Random(t *testing.T) {
+	type args struct {
+	}
+	fontFamily := NewFontFamily()
+	tests := []struct {
+		name     string
+		t        *FontFamily
+		args     args
+		fn       func(*FontFamily) error
+		wantErr  bool
+		wantFont bool
+	}{
+		{
+			name: "test1",
+			t:    fontFamily,
+			args: args{},
+			fn: func(family *FontFamily) error {
+				return nil
+			},
+			wantErr:  true,
+			wantFont: false,
+		},
+		{
+			name: "test2",
+			t:    fontFamily,
+			args: args{},
+			fn: func(family *FontFamily) error {
+				family.fonts = append(family.fonts, "./testdata/Hiragino Sans GB.ttc")
+				return nil
+			},
+			wantErr:  true,
+			wantFont: false,
+		},
+		{
+			name: "test3",
+			t:    fontFamily,
+			args: args{},
+			fn: func(family *FontFamily) error {
+				family.fonts = []string{"./fonts/3Dumb.ttf"}
+				return nil
+			},
+			wantErr:  false,
+			wantFont: true,
+		},
+		{
+			name: "test4",
+			t:    fontFamily,
+			args: args{},
+			fn: func(family *FontFamily) error {
+				return family.AddFont("./fonts/3Dumb.ttf")
+			},
+			wantErr:  false,
+			wantFont: true,
+		},
+		{
+			name: "test5",
+			t:    fontFamily,
+			args: args{},
+			fn: func(family *FontFamily) error {
+				return family.AddFont("./fonts/Comismsh.ttf")
+			},
+			wantErr:  false,
+			wantFont: true,
+		},
+		{
+			name: "test6",
+			t:    fontFamily,
+			args: args{},
+			fn: func(family *FontFamily) error {
+				return family.AddFont("./testdata/Hiragino Sans GB.ttc")
+			},
+			wantErr:  false,
+			wantFont: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_ = tt.fn(tt.t)
+
+			if font, err := tt.t.Random(); (font == nil) == tt.wantFont || (err != nil) != tt.wantErr {
+				t.Errorf("FontFamily.Random() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestSetFonts(t *testing.T) {
+	type args struct {
+		fonts []string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			args: args{
+				fonts: []string{"./fonts/3Dumb.ttf"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "test2",
+			args: args{
+				fonts: []string{"./testdata/Hiragino Sans GB.ttc"},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := SetFonts(tt.args.fonts...); (err != nil) != tt.wantErr {
+				t.Errorf("SetFonts() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestFontFamily_AddFontPath(t *testing.T) {
+	type args struct {
+		dirPath string
+	}
+	fontFamily := NewFontFamily()
+	tests := []struct {
+		name    string
+		t       *FontFamily
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "test1",
+			t:       fontFamily,
+			args:    args{dirPath: "./fonts"},
+			wantErr: false,
+		},
+		{
+			name:    "test2",
+			t:       fontFamily,
+			args:    args{dirPath: "./testdata/not_exist"},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.AddFontPath(tt.args.dirPath); (err != nil) != tt.wantErr {
+				t.Errorf("FontFamily.AddFontPath() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

BIN
gocaptcha/fonts/3Dumb.ttf


BIN
gocaptcha/fonts/ApothecaryFont.ttf


BIN
gocaptcha/fonts/Comismsh.ttf


+ 0 - 0
captcha/fonts/D3Parallelism.ttf → gocaptcha/fonts/D3Parallelism.ttf


BIN
gocaptcha/fonts/DENNEthree-dee.ttf


BIN
gocaptcha/fonts/DeborahFancyDress.ttf


BIN
gocaptcha/fonts/Esquisito.ttf


+ 0 - 0
captcha/fonts/Flim-Flam.ttf → gocaptcha/fonts/Flim-Flam.ttf


BIN
gocaptcha/fonts/KREMLINGEORGIANI3D.ttf


BIN
gocaptcha/fonts/actionj.ttf


+ 0 - 0
captcha/fonts/chromohv.ttf → gocaptcha/fonts/chromohv.ttf


+ 255 - 0
gocaptcha/line.go

@@ -0,0 +1,255 @@
+package gocaptcha
+
+import (
+	"image"
+	"image/color"
+	"image/draw"
+	"math"
+	"math/rand"
+	"time"
+)
+
+// LineDrawer 实现划线的接口
+type LineDrawer interface {
+	DrawLine(canvas draw.Image, x image.Point, y image.Point, color color.Color) error
+}
+
+type beeline struct {
+}
+
+func NewBeeline() LineDrawer {
+	return &beeline{}
+}
+
+// DrawLine 画一条直线
+func (beeline) DrawLine(canvas draw.Image, x1 image.Point, y1 image.Point, color color.Color) error {
+	dx := abs(x1.X - y1.X)
+	dy := abs(y1.Y - x1.Y)
+
+	sx, sy := 1, 1
+	if x1.X >= y1.X {
+		sx = -1
+	}
+	if x1.Y >= y1.Y {
+		sy = -1
+	}
+	err := dx - dy
+	x, y := x1.X, x1.Y
+
+	// 预定义粗线的相对偏移
+	offsets := []struct{ dx, dy int }{
+		{-1, -1}, {0, -1}, {1, -1},
+		{-1, 0}, {0, 0}, {1, 0},
+		{-1, 1}, {0, 1}, {1, 1},
+	}
+
+	// 边界检查函数
+	isValidPoint := func(cx, cy int) bool {
+		return cx >= 0 && cx < canvas.Bounds().Dx() && cy >= 0 && cy < canvas.Bounds().Dy()
+	}
+
+	// 绘制粗点函数
+	drawThickPoint := func(cx, cy int) {
+		for _, offset := range offsets {
+			nx, ny := cx+offset.dx, cy+offset.dy
+			if isValidPoint(nx, ny) {
+				canvas.Set(nx, ny, color)
+			}
+		}
+	}
+
+	// 主循环
+	for {
+		drawThickPoint(x, y) // 绘制粗点
+		if x == y1.X && y == y1.Y {
+			break // 到达终点,退出循环
+		}
+		e2 := 2 * err
+		if e2 > -dy {
+			err -= dy
+			x += sx
+		}
+		if e2 < dx {
+			err += dx
+			y += sy
+		}
+	}
+	return nil
+}
+
+type curveLine struct {
+	r *rand.Rand
+}
+
+func (c curveLine) DrawLine(canvas draw.Image, x image.Point, y image.Point, cl color.Color) error {
+	px := 0
+	var py float64 = 0
+
+	//振幅
+	amplitude := c.r.Intn(canvas.Bounds().Dy() / 2)
+
+	//Y轴方向偏移量
+	b := Random(int64(-canvas.Bounds().Dy()/4), int64(canvas.Bounds().Dy()/4))
+
+	//X轴方向偏移量
+	frequency := Random(int64(-canvas.Bounds().Dy()/4), int64(canvas.Bounds().Dy()/4))
+	// 周期
+	var t float64 = 0
+	if canvas.Bounds().Dy() > canvas.Bounds().Dx()/2 {
+		t = Random(int64(canvas.Bounds().Dx()/2), int64(canvas.Bounds().Dy()))
+	} else {
+		t = Random(int64(canvas.Bounds().Dy()), int64(canvas.Bounds().Dx()/2))
+	}
+	// 相位
+	phase := (2 * math.Pi) / t
+
+	// 曲线横坐标起始位置
+	px1 := 0
+	px2 := int(Random(int64(float64(canvas.Bounds().Dx())*0.8), int64(canvas.Bounds().Dx())))
+
+	for px = px1; px < px2; px++ {
+		if phase != 0 {
+			py = float64(amplitude)*math.Sin(phase*float64(px)+frequency) + b + (float64(canvas.Bounds().Dx()) / float64(5))
+			i := canvas.Bounds().Dy() / 5
+			for i > 0 {
+				canvas.Set(px+i, int(py), cl)
+				i--
+			}
+		}
+	}
+	return nil
+}
+
+// NewCurveLine 基于正弦函数的曲线
+func NewCurveLine() LineDrawer {
+	return &curveLine{
+		r: rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}
+
+type bezierLine struct {
+	r *rand.Rand
+}
+
+func (b bezierLine) DrawLine(canvas draw.Image, p0 image.Point, p2 image.Point, curveColor color.Color) error {
+	width := canvas.Bounds().Dx()
+	height := canvas.Bounds().Dy()
+	// 随机生成4个控制点
+	//p0 := image.Point{X: rand.Intn(width / 4), Y: rand.Intn(height)}
+	p1 := image.Point{X: b.r.Intn(width / 2), Y: b.r.Intn(height)}
+	//p2 := image.Point{X: width/2 + rand.Intn(width/4), Y: rand.Intn(height)}
+	p3 := image.Point{X: width - 1, Y: b.r.Intn(height)}
+
+	// 绘制贝塞尔曲线
+	for t := 0.0; t <= 1.0; t += 0.001 {
+		x := int((1-t)*(1-t)*(1-t)*float64(p0.X) + 3*(1-t)*(1-t)*t*float64(p1.X) + 3*(1-t)*t*t*float64(p2.X) + t*t*t*float64(p3.X))
+		y := int((1-t)*(1-t)*(1-t)*float64(p0.Y) + 3*(1-t)*(1-t)*t*float64(p1.Y) + 3*(1-t)*t*t*float64(p2.Y) + t*t*t*float64(p3.Y))
+		canvas.Set(x, y, curveColor)
+	}
+	return nil
+}
+
+// NewBezierLine 贝塞尔曲线
+func NewBezierLine() LineDrawer {
+	return &bezierLine{
+		r: rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}
+
+type bezier3DLine struct {
+	r *rand.Rand
+}
+
+// DrawLine 绘制3D效果的贝塞尔曲线
+func (b bezier3DLine) DrawLine(canvas draw.Image, p0 image.Point, p2 image.Point, cl color.Color) error {
+	width := canvas.Bounds().Dx()
+	height := canvas.Bounds().Dy()
+	// 随机生成4个控制点
+	//p0 := image.Point{X: b.r.Intn(width / 4), Y: b.r.Intn(height)}
+	p1 := image.Point{X: b.r.Intn(width / 2), Y: b.r.Intn(height)}
+	//p2 := image.Point{X: width/2 + b.r.Intn(width/4), Y: b.r.Intn(height)}
+	p3 := image.Point{X: width - 1, Y: b.r.Intn(height)}
+
+	drawPointWithWidth := func(img draw.Image, x, y int, col color.Color, width int) {
+		for dx := -width; dx <= width; dx++ {
+			for dy := -width; dy <= width; dy++ {
+				// 确保点在圆形范围内
+				if dx*dx+dy*dy <= width*width {
+					img.Set(x+dx, y+dy, col)
+				}
+			}
+		}
+	}
+	w := float64(b.r.Intn(height / 5))
+	// 绘制贝塞尔曲线,模拟3D效果
+	for t := 0.0; t <= 1.0; t += 0.001 {
+		// 计算当前点的坐标
+		x := int((1-t)*(1-t)*(1-t)*float64(p0.X) + 3*(1-t)*(1-t)*t*float64(p1.X) + 3*(1-t)*t*t*float64(p2.X) + t*t*t*float64(p3.X))
+		y := int((1-t)*(1-t)*(1-t)*float64(p0.Y) + 3*(1-t)*(1-t)*t*float64(p1.Y) + 3*(1-t)*t*t*float64(p2.Y) + t*t*t*float64(p3.Y))
+
+		// 使用 t 值调整颜色和线宽,模拟3D效果
+		opacity := uint8(255 - int(t*255)) // 透明度渐变
+		lineColor := color.NRGBA{
+			R: uint8(250 * t), // 红色分量随 t 增加
+			G: uint8(128 * (1 - t)),
+			B: 255 - uint8(128*t),
+			A: opacity,
+		}
+
+		// 模拟线宽,绘制当前点周围的像素
+		lineWidth := int(w * (1 - t)) // 线宽随 t 减小
+		drawPointWithWidth(canvas, x, y, lineColor, lineWidth)
+	}
+	return nil
+}
+
+// NewBezier3DLine 3D效果的贝塞尔曲线
+func NewBezier3DLine() LineDrawer {
+	return &bezier3DLine{
+		r: rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}
+
+type hollowLine struct {
+	r *rand.Rand
+}
+
+// DrawLine 绘制空心线
+func (h hollowLine) DrawLine(canvas draw.Image, p0 image.Point, p1 image.Point, lineColor color.Color) error {
+	bounds := canvas.Bounds()
+	width := bounds.Dx()
+	height := bounds.Dy()
+
+	x1 := float64(p0.X)
+	x2 := float64(p1.X)
+
+	multiple := float64(h.r.Intn(5)+3) / 5.0
+	if int(multiple*10)%3 == 0 {
+		multiple = multiple * -1.0
+	}
+
+	w := width / 20
+
+	for ; x1 < x2; x1++ {
+		y := math.Sin(x1*math.Pi*multiple/float64(width)) * float64(height/3)
+
+		if multiple < 0 {
+			y = y + float64(height/2)
+		}
+
+		// Ensure y is within bounds
+		y = math.Max(0, math.Min(float64(height-1), y))
+
+		for i := 0; i <= w && int(y)+i < height; i++ {
+			canvas.Set(int(x1), int(y)+i, lineColor)
+		}
+	}
+	return nil
+}
+
+// NewHollowLine 空心线
+func NewHollowLine() LineDrawer {
+	return &hollowLine{
+		r: rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}

+ 144 - 0
gocaptcha/line_test.go

@@ -0,0 +1,144 @@
+package gocaptcha
+
+import (
+	"image"
+	"image/color"
+	"image/draw"
+	"testing"
+)
+
+func Test_Beeline(t *testing.T) {
+	type args struct {
+		canvas    draw.Image
+		x         image.Point
+		y         image.Point
+		lineColor color.Color
+	}
+	tests := []struct {
+		name    string
+		t       LineDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    NewBeeline(),
+			args: args{
+				canvas:    image.NewRGBA(image.Rect(0, 0, 10, 10)),
+				x:         image.Point{X: 11, Y: 5},
+				y:         image.Point{X: 10, Y: 5},
+				lineColor: color.RGBA{},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawLine(tt.args.canvas, tt.args.x, tt.args.y, tt.args.lineColor); (err != nil) != tt.wantErr {
+				t.Errorf("beeline.DrawLine() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func Test_CurveLine(t *testing.T) {
+	type args struct {
+		canvas    draw.Image
+		x         image.Point
+		y         image.Point
+		lineColor color.Color
+	}
+	tests := []struct {
+		name    string
+		t       LineDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    NewCurveLine(),
+			args: args{
+				canvas:    image.NewRGBA(image.Rect(0, 0, 10, 10)),
+				x:         image.Point{X: 11, Y: 5},
+				y:         image.Point{X: 10, Y: 5},
+				lineColor: color.RGBA{},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawLine(tt.args.canvas, tt.args.x, tt.args.y, tt.args.lineColor); (err != nil) != tt.wantErr {
+				t.Errorf("beeline.DrawLine() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func Test_BezierLine(t *testing.T) {
+	type args struct {
+		canvas    draw.Image
+		x         image.Point
+		y         image.Point
+		lineColor color.Color
+	}
+	tests := []struct {
+		name    string
+		t       LineDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    NewBezierLine(),
+			args: args{
+				canvas:    image.NewRGBA(image.Rect(0, 0, 10, 10)),
+				x:         image.Point{X: 11, Y: 5},
+				y:         image.Point{X: 10, Y: 5},
+				lineColor: color.RGBA{},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawLine(tt.args.canvas, tt.args.x, tt.args.y, tt.args.lineColor); (err != nil) != tt.wantErr {
+				t.Errorf("beeline.DrawLine() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func Test_HollowLine(t *testing.T) {
+	type args struct {
+		canvas    draw.Image
+		x         image.Point
+		y         image.Point
+		lineColor color.Color
+	}
+	tests := []struct {
+		name    string
+		t       LineDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    NewHollowLine(),
+			args: args{
+				canvas:    image.NewRGBA(image.Rect(0, 0, 10, 10)),
+				x:         image.Point{X: 1, Y: 5},
+				y:         image.Point{X: 10, Y: 5},
+				lineColor: color.RGBA{},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawLine(tt.args.canvas, tt.args.x, tt.args.y, tt.args.lineColor); (err != nil) != tt.wantErr {
+				t.Errorf("beeline.DrawLine() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 135 - 0
gocaptcha/noise.go

@@ -0,0 +1,135 @@
+package gocaptcha
+
+import (
+	"image"
+	"image/draw"
+	"math/rand"
+	"time"
+
+	"github.com/golang/freetype"
+	"golang.org/x/image/font"
+)
+
+// NoiseDensity is the complexity of captcha
+type NoiseDensity int
+
+const (
+	NoiseDensityLower NoiseDensity = iota
+	NoiseDensityMedium
+	NoiseDensityHigh
+)
+
+// NoiseDrawer is a type that can make noise on an image
+type NoiseDrawer interface {
+	// DrawNoise draws noise on the image
+	DrawNoise(img draw.Image, density NoiseDensity, rn int) error
+}
+
+type pointNoiseDrawer struct {
+	r *rand.Rand
+}
+
+// DrawNoise draws noise on the image
+func (n pointNoiseDrawer) DrawNoise(img draw.Image, density NoiseDensity, rn int) error {
+	var densityNum int
+	switch density {
+	case NoiseDensityLower:
+		densityNum = 28
+	case NoiseDensityMedium:
+		densityNum = 18
+	case NoiseDensityHigh:
+		densityNum = 8
+	default:
+		densityNum = 18
+	}
+
+	maxSize := (img.Bounds().Dy() * img.Bounds().Dx()) / densityNum
+
+	bounds := img.Bounds()
+	width := bounds.Dx()
+	height := bounds.Dy()
+
+	for i := 0; i < maxSize; i++ {
+		rw := n.r.Intn(width)
+		rh := n.r.Intn(height)
+
+		img.Set(rw, rh, RandColor())
+		// 优化噪声点的生成逻辑,例如可以基于一定的概率决定是否绘制额外的点
+		if n.r.Intn(3) == 0 && rw+1 < width && rh+1 < height {
+			img.Set(rw+1, rh+1, RandColor())
+		}
+	}
+	return nil
+}
+
+// NewPointNoiseDrawer returns a NoiseDrawer that draws noise points
+func NewPointNoiseDrawer() NoiseDrawer {
+	return &pointNoiseDrawer{
+		r: rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}
+
+// textNoiseDrawer draws noise text
+type textNoiseDrawer struct {
+	r   *rand.Rand
+	dpi float64
+}
+
+// DrawNoise draws noise on the image
+func (n textNoiseDrawer) DrawNoise(img draw.Image, density NoiseDensity, rn int) error {
+	var densityNum int
+	switch density {
+	case NoiseDensityLower:
+		densityNum = 2000
+	case NoiseDensityMedium:
+		densityNum = 1500
+	case NoiseDensityHigh:
+		densityNum = 1000
+	default:
+		densityNum = 1500 // 默认值
+	}
+	bounds := img.Bounds()
+	maxSize := (bounds.Dy() * bounds.Dx()) / densityNum
+	c := freetype.NewContext()
+	if n.dpi <= 0 {
+		n.dpi = 72
+	}
+
+	c.SetDPI(n.dpi)
+
+	c.SetClip(bounds)
+	c.SetDst(img)
+	c.SetHinting(font.HintingFull)
+	rawFontSize := float64(bounds.Dy()) / (1 + float64(n.r.Intn(7))/float64(10))
+
+	for i := 0; i < maxSize; i++ {
+
+		rw := n.r.Intn(bounds.Dx())
+		rh := n.r.Intn(bounds.Dy())
+
+		text := RandText(1)
+		fontSize := rawFontSize/2 + float64(n.r.Intn(5))
+
+		c.SetSrc(image.NewUniform(RandLightColor(rn)))
+		c.SetFontSize(fontSize)
+		f, err := DefaultFontFamily.Random()
+		if err != nil {
+			return err
+		}
+		c.SetFont(f)
+		pt := freetype.Pt(rw, rh)
+
+		_, err = c.DrawString(text, pt)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func NewTextNoiseDrawer(dpi float64) NoiseDrawer {
+	return &textNoiseDrawer{
+		r:   rand.New(rand.NewSource(time.Now().UnixNano())),
+		dpi: dpi,
+	}
+}

+ 128 - 0
gocaptcha/noise_test.go

@@ -0,0 +1,128 @@
+package gocaptcha
+
+import (
+	"image"
+	"image/draw"
+	"math/rand"
+	"testing"
+)
+
+func Test_PointNoiseDrawer(t *testing.T) {
+	type args struct {
+		canvas  draw.Image
+		density NoiseDensity
+	}
+	tests := []struct {
+		name    string
+		t       NoiseDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    &pointNoiseDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensityLower,
+			},
+			wantErr: false,
+		},
+		{
+			name: "test2",
+			t:    &pointNoiseDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensityMedium,
+			},
+			wantErr: false,
+		},
+		{
+			name: "test3",
+			t:    &pointNoiseDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensityHigh,
+			},
+			wantErr: false,
+		},
+		{
+			name: "test4",
+			t:    NewPointNoiseDrawer(),
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensity(4),
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawNoise(tt.args.canvas, tt.args.density, Rn); (err != nil) != tt.wantErr {
+				t.Errorf("pointNoiseDrawer.DrawNoise() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func Test_TextNoiseDrawer(t *testing.T) {
+	type args struct {
+		canvas  draw.Image
+		density NoiseDensity
+	}
+	tests := []struct {
+		name    string
+		t       NoiseDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "test1",
+			t:    &textNoiseDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensityLower,
+			},
+			wantErr: false,
+		},
+		{
+			name: "test2",
+			t:    &textNoiseDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensityMedium,
+			},
+			wantErr: false,
+		},
+		{
+			name: "test3",
+			t:    &textNoiseDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensityHigh,
+			},
+			wantErr: false,
+		},
+		{
+			name: "test4",
+			t:    NewTextNoiseDrawer(72),
+			args: args{
+				canvas:  image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				density: NoiseDensity(4),
+			},
+			wantErr: false,
+		},
+	}
+
+	err := DefaultFontFamily.AddFontPath("./fonts")
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawNoise(tt.args.canvas, tt.args.density, Rn); (err != nil) != tt.wantErr {
+				t.Errorf("textNoiseDrawer.DrawNoise() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 24 - 0
gocaptcha/rnd.go

@@ -0,0 +1,24 @@
+package gocaptcha
+
+import (
+	"fmt"
+	"math/rand"
+)
+
+// Random 生成指定大小的随机数.
+func Random(min int64, max int64) float64 {
+	if max <= min {
+		panic(fmt.Sprintf("invalid range %d <= %d", max, min)) // 修复了错误消息的顺序
+	}
+
+	rangeSize := max - min
+	var randomValue int64
+
+	if rangeSize > 0 {
+		randomValue = rand.Int63n(rangeSize) + min // 确保在[min, max)范围内
+	} else { // 处理负数范围
+		randomValue = rand.Int63n(-rangeSize) + min // 确保在[min, max)范围内,此时max是更小的负数
+	}
+
+	return float64(randomValue) + rand.Float64() // 添加小数部分
+}

+ 11 - 0
gocaptcha/rnd_test.go

@@ -0,0 +1,11 @@
+package gocaptcha
+
+import (
+	"testing"
+)
+
+func TestRandom(t *testing.T) {
+	for i := 0; i < 100; i++ {
+		t.Log(Random(0, 1))
+	}
+}

+ 108 - 0
gocaptcha/start.go

@@ -0,0 +1,108 @@
+package gocaptcha
+
+import (
+	"app.yhyue.com/moapp/jybase/go-xweb/httpsession"
+	"app.yhyue.com/moapp/jybase/redis"
+	"fmt"
+	"image/color"
+	"log"
+	"net/http"
+
+	"math/rand"
+)
+
+// 图形验证码大小
+const (
+	dx = 90
+	dy = 30
+	Rn = 1
+)
+
+// 生成的字符集
+var TextCharacters = []rune("ACDEFGHJKLMNPQRSTUVWXY2456789")
+
+// 初始化字体文件
+func InitCaptcha() {
+	err := SetFontPath("./fonts/")
+	if err != nil {
+		log.Println(err)
+	}
+}
+
+// RandText 生成随机字体.
+func RandText(num int) string {
+	textNum := len(TextCharacters)
+	text := make([]rune, num)
+	for i := 0; i < num; i++ {
+		text[i] = TextCharacters[rand.Intn(textNum)]
+	}
+	return string(text)
+}
+
+// RandLightColor 随机生成浅色底色.
+func RandLightColor(n int) color.RGBA {
+	// 为每个颜色分量生成一个128到255之间的随机数
+	red := rand.Intn(100/n) + 156 + (100 - 100/n)
+	green := rand.Intn(100/n) + 156 + (100 - 100/n)
+	blue := rand.Intn(100/n) + 156 + (100 - 100/n)
+	// Alpha 通道设置为完全不透明
+	a := uint8(rand.Intn(8) + 247)
+
+	return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: a}
+}
+
+func Get(sess *httpsession.Session, w http.ResponseWriter, r *http.Request) {
+	str := RandText(4)
+	log.Println(str)
+	cacheKey := fmt.Sprintf("captcha_times_%s", sess.Id())
+	var (
+		// 计次数
+		times = redis.GetInt("other", cacheKey)
+		ttl   = redis.GetTTL("other", cacheKey)
+		rn    = times%6 + 1
+		//跳色
+		cc = RandLightColor(rn) //
+	)
+	// cc.A = uint8(rand.Intn(10) + 245)
+	captchaImage := New(dx, dy, cc).
+		DrawBorder(RandDeepColor())
+	if times < 3 {
+		captchaImage = captchaImage.DrawNoise(NoiseDensityLower, NewPointNoiseDrawer(), rn).
+			DrawNoise(NoiseDensityHigh, NewTextNoiseDrawer(42), rn).
+			DrawLine(NewBeeline(), RandDeepColor()).
+			DrawText(NewTwistTextDrawer(63, 15, 0.04), str)
+	} else if times < 6 {
+		captchaImage = captchaImage.DrawNoise(NoiseDensityLower, NewPointNoiseDrawer(), rn).
+			DrawNoise(NoiseDensityHigh, NewTextNoiseDrawer(43), rn).
+			DrawLine(NewBeeline(), RandDeepColor()).
+			DrawText(NewTwistTextDrawer(65, 14, 0.03), str).
+			DrawBlur(NewGaussianBlur(), 2, 0.15)
+	} else if times < 10 {
+		captchaImage = captchaImage.DrawNoise(NoiseDensityLower, NewPointNoiseDrawer(), rn).
+			DrawNoise(NoiseDensityHigh, NewTextNoiseDrawer(45), rn).
+			DrawLine(NewBeeline(), RandDeepColor()).
+			DrawText(NewTwistTextDrawer(66, 18, 0.04), str).
+			DrawLine(NewBeeline(), RandDeepColor()).
+			DrawBlur(NewGaussianBlur(), 2, 0.41)
+
+	} else {
+		captchaImage = captchaImage.DrawNoise(NoiseDensityHigh, NewTextNoiseDrawer(50), rn).
+			DrawNoise(NoiseDensityLower, NewPointNoiseDrawer(), rn).
+			DrawLine(NewBezier3DLine(), RandLightColor(rn)).
+			DrawLine(NewBeeline(), RandDeepColor()).
+			DrawText(NewTwistTextDrawer(66, 18, 0.04), str).
+			DrawBlur(NewGaussianBlur(), 2, 0.44)
+	}
+	if times == 0 {
+		ttl = 24 * 60 * 60
+	}
+	//计次
+	times++
+	redis.Put("other", cacheKey, times, int(ttl))
+	if captchaImage.Error != nil {
+		log.Println(captchaImage.Error)
+	}
+
+	_ = captchaImage.Encode(w, ImageFormatJpeg)
+
+}

+ 2 - 2
captcha/captcha_test.go → gocaptcha/start_test.go

@@ -1,4 +1,4 @@
-package captcha
+package gocaptcha
 
 import (
 	"app.yhyue.com/moapp/jybase/go-xweb/httpsession"
@@ -10,7 +10,7 @@ import (
 
 func TestCaptcha(t *testing.T) {
 	redis.InitRedisBySize("other=172.20.45.129:1712", 100, 30, 300)
-	CaptchaInit()
+	InitCaptcha()
 	var (
 		W       http.ResponseWriter
 		R       *http.Request

BIN
gocaptcha/testdata/Hiragino Sans GB.ttc


+ 173 - 0
gocaptcha/text.go

@@ -0,0 +1,173 @@
+package gocaptcha
+
+import (
+	"errors"
+	"image"
+	"image/draw"
+	"math"
+	"math/rand"
+	"time"
+
+	"github.com/golang/freetype"
+	"golang.org/x/image/font"
+)
+
+var (
+	ErrNilCanvas = errors.New("canvas is nil")
+	ErrNilText   = errors.New("text is nil")
+)
+
+// TextDrawer is a text drawer interface.
+type TextDrawer interface {
+	DrawString(canvas draw.Image, text string) error
+}
+
+type textDrawer struct {
+	dpi float64
+	r   *rand.Rand
+}
+
+// DrawString draws a string on the canvas.
+func (t *textDrawer) DrawString(canvas draw.Image, text string) error {
+	if len(text) == 0 {
+		return ErrNilText
+	}
+	if canvas == nil {
+		return ErrNilCanvas
+	}
+	c := freetype.NewContext()
+	if t.dpi <= 0 {
+		t.dpi = 72
+	}
+	c.SetDPI(t.dpi)
+	c.SetClip(canvas.Bounds())
+	c.SetDst(canvas)
+	c.SetHinting(font.HintingFull)
+
+	fontWidth := canvas.Bounds().Dx() / len(text)
+
+	for i, s := range text {
+
+		fontSize := float64(canvas.Bounds().Dy()) / (1 + float64(t.r.Intn(7))/float64(9))
+
+		c.SetSrc(image.NewUniform(RandDeepColor()))
+		c.SetFontSize(fontSize)
+		f, err := DefaultFontFamily.Random()
+
+		if err != nil {
+			return err
+		}
+		c.SetFont(f)
+
+		x := (fontWidth)*i + (fontWidth)/int(fontSize)
+
+		y := 5 + t.r.Intn(canvas.Bounds().Dy()/2) + int(fontSize/2)
+
+		pt := freetype.Pt(x, y)
+
+		_, err = c.DrawString(string(s), pt)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// NewTextDrawer returns a new text drawer.
+func NewTextDrawer(dpi float64) TextDrawer {
+	return &textDrawer{
+		dpi: dpi,
+		r:   rand.New(rand.NewSource(time.Now().UnixNano())),
+	}
+}
+
+type twistTextDrawer struct {
+	dpi       float64
+	r         *rand.Rand
+	amplitude float64
+	frequency float64
+}
+
+// DrawString draws a string on the canvas.
+func (t *twistTextDrawer) DrawString(canvas draw.Image, text string) error {
+	if len(text) == 0 {
+		return ErrNilText
+	}
+	if canvas == nil {
+		return ErrNilCanvas
+	}
+	// 创建一个新的画布用于存储扭曲后的图像
+	textCanvas := image.NewRGBA(image.Rect(0, 0, canvas.Bounds().Dx(), canvas.Bounds().Dy()))
+	draw.Draw(textCanvas, textCanvas.Bounds(), image.Transparent, image.Point{}, draw.Src)
+
+	c := freetype.NewContext()
+	if t.dpi <= 0 {
+		t.dpi = 72
+	}
+	c.SetDPI(t.dpi)
+	c.SetClip(canvas.Bounds())
+	c.SetDst(textCanvas)
+	c.SetHinting(font.HintingFull)
+
+	fontWidth := canvas.Bounds().Dx() / len(text)
+
+	for i, s := range text {
+
+		fontSize := float64(canvas.Bounds().Dy()) / (1 + float64(t.r.Intn(7))/float64(9))
+
+		c.SetSrc(image.NewUniform(RandDeepColor()))
+		c.SetFontSize(fontSize)
+		f, err := DefaultFontFamily.Random()
+
+		if err != nil {
+			return err
+		}
+		c.SetFont(f)
+
+		x := (fontWidth)*i + (fontWidth)/int(fontSize)
+
+		y := 5 + t.r.Intn(canvas.Bounds().Dy()/2) + int(fontSize/2)
+
+		pt := freetype.Pt(x, y)
+
+		_, err = c.DrawString(string(s), pt)
+		if err != nil {
+			return err
+		}
+	}
+	return t.twistEffect(textCanvas, canvas)
+}
+
+func (t *twistTextDrawer) twistEffect(src image.Image, dst draw.Image) error {
+	width := src.Bounds().Dx()
+	height := src.Bounds().Dy()
+
+	// 遍历源图像像素
+	for y := 0; y < height; y++ {
+		for x := 0; x < width; x++ {
+			// 计算扭曲后的坐标
+			dx := int(t.amplitude * math.Sin(t.frequency*float64(y)))
+			newX := x + dx
+			newY := y
+
+			// 如果新坐标在目标图像范围内,设置像素
+			if newX >= 0 && newX < width && newY >= 0 && newY < height {
+				_, _, _, a := src.At(x, y).RGBA()
+				if a != 0 {
+					dst.Set(newX, newY, src.At(x, y))
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// NewTwistTextDrawer returns a new text drawer with twist effect.
+func NewTwistTextDrawer(dpi float64, amplitude float64, frequency float64) TextDrawer {
+	return &twistTextDrawer{
+		dpi:       dpi,
+		r:         rand.New(rand.NewSource(time.Now().UnixNano())),
+		amplitude: amplitude,
+		frequency: frequency,
+	}
+}

+ 112 - 0
gocaptcha/text_test.go

@@ -0,0 +1,112 @@
+package gocaptcha
+
+import (
+	"image"
+	"image/draw"
+	"math/rand"
+	"testing"
+)
+
+func Test_textDrawer_DrawString(t *testing.T) {
+	type args struct {
+		canvas draw.Image
+		text   string
+	}
+	tests := []struct {
+		name    string
+		t       *textDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "Successful DrawString",
+			t:    &textDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas: image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				text:   "Hello, World!",
+			},
+			wantErr: false,
+		},
+		{
+			name: "DrawString with empty text",
+			t:    &textDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas: image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				text:   "",
+			},
+			wantErr: true,
+		},
+		{
+			name: "DrawString with nil canvas",
+			t:    &textDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas: nil,
+				text:   "Hello, World!",
+			},
+			wantErr: true,
+		},
+	}
+	err := DefaultFontFamily.AddFontPath("./fonts")
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawString(tt.args.canvas, tt.args.text); (err != nil) != tt.wantErr {
+				t.Errorf("textDrawer.DrawString() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func Test_twistTextDrawer_DrawString(t *testing.T) {
+	type args struct {
+		canvas draw.Image
+		text   string
+	}
+	tests := []struct {
+		name    string
+		t       *twistTextDrawer
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "Successful DrawString",
+			t:    &twistTextDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas: image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				text:   "Hello, World!",
+			},
+			wantErr: false,
+		},
+		{
+			name: "DrawString with empty text",
+			t:    &twistTextDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas: image.NewRGBA(image.Rect(0, 0, 100, 100)),
+				text:   "",
+			},
+			wantErr: true,
+		},
+		{
+			name: "DrawString with nil canvas",
+			t:    &twistTextDrawer{r: rand.New(rand.NewSource(1))},
+			args: args{
+				canvas: nil,
+				text:   "Hello, World!",
+			},
+			wantErr: true,
+		},
+	}
+	err := DefaultFontFamily.AddFontPath("./fonts")
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.t.DrawString(tt.args.canvas, tt.args.text); (err != nil) != tt.wantErr {
+				t.Errorf("twistTextDrawer.DrawString() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 99 - 0
gocaptcha/utils.go

@@ -0,0 +1,99 @@
+package gocaptcha
+
+import (
+	"image/color"
+	"math/rand"
+)
+
+// RandDeepColor 随机生成深色系.
+func RandDeepColor() color.RGBA {
+	// 限制 RGB 最大值为 150 (深色系),最小值为 50
+	maxValue := 150
+	minValue := 50
+
+	r := uint8(rand.Intn(maxValue-minValue+1) + minValue)
+	g := uint8(rand.Intn(maxValue-minValue+1) + minValue)
+	b := uint8(rand.Intn(maxValue-minValue+1) + minValue)
+
+	// Alpha 通道设置为完全不透明
+	a := uint8(rand.Intn(256))
+
+	return color.RGBA{R: r, G: g, B: b, A: a}
+}
+
+// RandLightColor 随机生成浅色.
+//func RandLightColor() color.RGBA {
+//	// 为每个颜色分量生成一个128到255之间的随机数
+//	red := rand.Intn(128) + 128
+//	green := rand.Intn(128) + 128
+//	blue := rand.Intn(128) + 128
+//	// Alpha 通道设置为完全不透明
+//	a := uint8(rand.Intn(256))
+//
+//	return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: a}
+//}
+
+// RandColor 生成随机颜色.
+func RandColor() color.RGBA {
+	red := rand.Intn(255)
+	green := rand.Intn(255)
+	var blue int
+
+	// Calculate blue value based on the sum of red and green
+	sum := red + green
+	if sum > 400 {
+		blue = 0
+	} else {
+		blueTemp := 400 - sum
+		blue = max(0, min(255, blueTemp))
+	}
+	return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: 255}
+}
+// RandText 生成随机字体.
+//func RandText(num int) string {
+//	textNum := len(TextCharacters)
+//	text := make([]rune, num)
+//	for i := 0; i < num; i++ {
+//		text[i] = TextCharacters[rand.Intn(textNum)]
+//	}
+//	return string(text)
+//}
+
+// ColorToRGB 颜色代码转换为RGB
+// input int
+// output int red, green, blue.
+func ColorToRGB(colorVal int) color.RGBA {
+
+	red := colorVal >> 16
+	green := (colorVal & 0x00FF00) >> 8
+	blue := colorVal & 0x0000FF
+
+	return color.RGBA{
+		R: uint8(red),
+		G: uint8(green),
+		B: uint8(blue),
+		A: uint8(255),
+	}
+}
+
+// 整数绝对值函数
+func abs[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64](a T) T {
+	var zero T
+	if a < zero {
+		return -a
+	}
+	return a
+}
+
+func max(x,y int) int{
+	if x<y{
+		return y
+	}
+	return x
+}
+func min(x , y int) int{
+	if x>y{
+		return y
+	}
+	return x
+}

+ 215 - 0
gocaptcha/utils_test.go

@@ -0,0 +1,215 @@
+package gocaptcha
+
+import (
+	"image/color"
+	"testing"
+)
+
+func Test_abs(t *testing.T) {
+	type args struct {
+		a int
+	}
+	tests := []struct {
+		name string
+		args args
+		want int
+	}{
+		{
+			name: "Positive number",
+			args: args{a: 10},
+			want: 10,
+		},
+		{
+			name: "Negative number",
+			args: args{a: -10},
+			want: 10,
+		},
+		{
+			name: "Zero",
+			args: args{a: 0},
+			want: 0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := abs(tt.args.a); got != tt.want {
+				t.Errorf("abs() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestColorToRGB(t *testing.T) {
+	type args struct {
+		colorVal int
+	}
+	tests := []struct {
+		name string
+		args args
+		want color.RGBA
+	}{
+		{
+			name: "Test 1",
+			args: args{colorVal: 0xFF0000},
+			want: color.RGBA{R: 255, G: 0, B: 0, A: 255},
+		},
+		{
+			name: "Test 2",
+			args: args{colorVal: 0x00FF00},
+			want: color.RGBA{R: 0, G: 255, B: 0, A: 255},
+		},
+		{
+			name: "Test 3",
+			args: args{colorVal: 0x0000FF},
+			want: color.RGBA{R: 0, G: 0, B: 255, A: 255},
+		},
+		{
+			name: "Test 4",
+			args: args{colorVal: 0xFFFFFF},
+			want: color.RGBA{R: 255, G: 255, B: 255, A: 255},
+		},
+		{
+			name: "Test 5",
+			args: args{colorVal: 0x000000},
+			want: color.RGBA{R: 0, G: 0, B: 0, A: 255},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ColorToRGB(tt.args.colorVal); got != tt.want {
+				t.Errorf("ColorToRGB() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestRandDeepColor(t *testing.T) {
+	type args struct {
+		colorVal int
+	}
+	tests := []struct {
+		name string
+		args args
+		want color.RGBA
+	}{
+		{
+			name: "Test 1",
+			args: args{colorVal: 0xFF0000},
+			want: color.RGBA{R: 50, G: 50, B: 50, A: 0},
+		},
+		{
+			name: "Test 2",
+			args: args{colorVal: 0x00FF00},
+			want: color.RGBA{R: 0, G: 50, B: 0, A: 0},
+		},
+		{
+			name: "Test 3",
+			args: args{colorVal: 0x0000FF},
+			want: color.RGBA{R: 0, G: 0, B: 50, A: 50},
+		},
+		{
+			name: "Test 4",
+			args: args{colorVal: 0xFFFFFF},
+			want: color.RGBA{R: 50, G: 50, B: 50, A: 50},
+		},
+		{
+			name: "Test 5",
+			args: args{colorVal: 0x000000},
+			want: color.RGBA{R: 0, G: 0, B: 0, A: 255},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := RandDeepColor(); got.R <= tt.want.R && got.G <= tt.want.G && got.B <= tt.want.B {
+				t.Errorf("RandDeepColor() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestRandLightColor(t *testing.T) {
+	type args struct {
+		colorVal int
+	}
+	tests := []struct {
+		name string
+		args args
+		want color.RGBA
+	}{
+		{
+			name: "Test 1",
+			args: args{colorVal: 0xFF0000},
+			want: color.RGBA{R: 128, G: 128, B: 128, A: 255},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := RandLightColor(Rn); got.R <= tt.want.R && got.G <= tt.want.G && got.B <= tt.want.B {
+				t.Errorf("RandLightColor() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestRandColor(t *testing.T) {
+	type args struct {
+		colorVal int
+	}
+	tests := []struct {
+		name string
+		args args
+		want color.RGBA
+	}{
+		{
+			name: "Test 1",
+			args: args{colorVal: 0xFF0000},
+			want: color.RGBA{R: 255, G: 255, B: 255, A: 255},
+		},
+		{
+			name: "Test 2",
+			args: args{colorVal: 0x00FF00},
+			want: color.RGBA{R: 255, G: 255, B: 255, A: 255},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := RandColor(); got.R >= tt.want.R && got.G >= tt.want.G && got.B >= tt.want.B {
+				t.Errorf("RandColor() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestRandText(t *testing.T) {
+	type args struct {
+		num int
+	}
+	tests := []struct {
+		name string
+		args args
+		want int
+	}{
+		{
+			name: "Test 1",
+			args: args{num: 1},
+			want: 1,
+		},
+		{
+			name: "Test 2",
+			args: args{num: 2},
+			want: 2,
+		},
+		{
+			name: "Test 3",
+			args: args{num: 3},
+			want: 3,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := RandText(tt.args.num); len(got) != tt.want {
+				t.Errorf("RandText() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}