Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
669a328c35 | |||
31a767886d | |||
8b04542f19 | |||
0d18ee4d3f | |||
98c666bea2 | |||
d294d79360 | |||
b4b097a927 | |||
|
c34288c982 | ||
|
2f847d5947 | ||
|
fef853ee5e | ||
|
1d4172a01f | ||
|
6b08f978b6 | ||
|
9234bd71dd | ||
|
f6fd454518 | ||
|
20014aab1b | ||
|
6aa88d953f | ||
|
019b34f8f2 | ||
|
80cb7ffd68 | ||
|
02532e6f4d | ||
|
ba2a083ab6 | ||
|
a4223da22a | ||
|
53e75b199a | ||
|
cccc7a97ea | ||
|
87b02acaf1 | ||
|
ba5dfca752 | ||
|
1bc08e3651 | ||
|
3915a04f79 | ||
|
b69ec48f20 | ||
|
8d7ec3aacd | ||
|
ade1a224fe | ||
|
244d68b51a | ||
|
3db110f2af | ||
|
26e89c7d47 | ||
|
f1f6487f0d | ||
|
8eb90511f0 | ||
|
06183fda34 | ||
|
12fb7f5809 | ||
|
2065fa60ee | ||
|
9ad0ec237f | ||
|
6905bb1079 | ||
|
b6f150856e | ||
|
5af4d9ea05 | ||
|
24252cb4f8 | ||
|
b06ff17030 | ||
|
7909ea661c | ||
|
e6742d643f |
15 changed files with 135 additions and 5135 deletions
11
.github/ISSUE_TEMPLATE.md
vendored
11
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,11 +0,0 @@
|
|||
**Do you want to request a *feature* or report a *bug*?**
|
||||
|
||||
**If this is a feature request, what is motivation for changing the behavior?**
|
||||
|
||||
**If it is a bug, which version of captcha are you using?**
|
||||
|
||||
**What is the current behavior?**
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
**Step to reproduce the bug or other relevant information**
|
16
.travis.yml
16
.travis.yml
|
@ -1,16 +0,0 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 - Present Weilin Shi
|
||||
Copyright (c) 2017 - 2024 Weilin Shi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
21
README.md
21
README.md
|
@ -2,19 +2,17 @@
|
|||
|
||||
<div>
|
||||
|
||||
[](https://godoc.org/github.com/steambap/captcha)
|
||||
[](https://travis-ci.org/steambap/captcha)
|
||||
[](https://codecov.io/gh/steambap/captcha)
|
||||
[](https://goreportcard.com/report/github.com/steambap/captcha)
|
||||
[](https://pkg.go.dev/concord.hectabit.org/HectaBit/captcha)
|
||||
[](https://goreportcard.com/report/concord.hectabit.org/HectaBit/captcha)
|
||||
|
||||
</div>
|
||||
|
||||
## Why another captcha generator?
|
||||
I want a simple and framework-independent way to generate captcha. It also should be flexible, at least allow me to pick my favorite font.
|
||||
Because I can.
|
||||
|
||||
## install
|
||||
```
|
||||
go get github.com/steambap/captcha
|
||||
go get concord.hectabit.org/HectaBit/captcha
|
||||
```
|
||||
|
||||
## usage
|
||||
|
@ -32,17 +30,22 @@ func handle(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
```
|
||||
|
||||
[documentation](https://godoc.org/github.com/steambap/captcha) |
|
||||
[example](example/basic/main.go)
|
||||
[documentation](https://pkg.go.dev/concord.hectabit.org/HectaBit/captcha) |
|
||||
[example](example/basic/main.go) |
|
||||
[font example](example/load-font/main.go)
|
||||
|
||||
## sample image
|
||||

|
||||
|
||||

|
||||
|
||||
## Compatibility
|
||||
|
||||
This package uses embed package from Go 1.16. If for some reasons you have to use pre 1.16 version of Go, reference pre 1.4 version of this module in your go.mod.
|
||||
|
||||
## Contributing
|
||||
If your found a bug, please contribute!
|
||||
see [contributing.md](contributing.md) for more detail.
|
||||
|
||||
## License
|
||||
[MIT](LICENSE.md)
|
||||
[MIT](LICENSE)
|
||||
|
|
97
captcha.go
97
captcha.go
|
@ -2,7 +2,7 @@
|
|||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed" // embed font
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
|
@ -22,7 +22,8 @@ import (
|
|||
|
||||
const charPreset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
//go:embed fonts/Comismsh.ttf
|
||||
var ttf []byte
|
||||
var ttfFont *truetype.Font
|
||||
|
||||
// Options manage captcha generation details.
|
||||
|
@ -61,7 +62,7 @@ func newDefaultOption(width, height int) *Options {
|
|||
return &Options{
|
||||
BackgroundColor: color.Transparent,
|
||||
CharPreset: charPreset,
|
||||
TextLength: 4,
|
||||
TextLength: 6,
|
||||
CurveNumber: 2,
|
||||
FontDPI: 72.0,
|
||||
FontScale: 1.0,
|
||||
|
@ -105,6 +106,7 @@ func (data *Data) WriteGIF(w io.Writer, o *gif.Options) error {
|
|||
|
||||
func init() {
|
||||
ttfFont, _ = freetype.ParseFont(ttf)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// LoadFont let you load an external font.
|
||||
|
@ -116,12 +118,12 @@ func LoadFont(fontData []byte) error {
|
|||
|
||||
// LoadFontFromReader load an external font from an io.Reader interface.
|
||||
func LoadFontFromReader(reader io.Reader) error {
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, reader); err != nil {
|
||||
b, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return LoadFont(buf.Bytes())
|
||||
return LoadFont(b)
|
||||
}
|
||||
|
||||
// New creates a new captcha.
|
||||
|
@ -134,11 +136,7 @@ func New(width int, height int, option ...SetOption) (*Data, error) {
|
|||
|
||||
text := randomText(options)
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{options.BackgroundColor}, image.ZP, draw.Src)
|
||||
drawNoise(img, options)
|
||||
drawCurves(img, options)
|
||||
err := drawText(text, img, options)
|
||||
if err != nil {
|
||||
if err := drawWithOption(text, img, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -155,21 +153,42 @@ func NewMathExpr(width int, height int, option ...SetOption) (*Data, error) {
|
|||
|
||||
text, equation := randomEquation()
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{options.BackgroundColor}, image.ZP, draw.Src)
|
||||
drawNoise(img, options)
|
||||
drawCurves(img, options)
|
||||
err := drawText(equation, img, options)
|
||||
if err != nil {
|
||||
if err := drawWithOption(equation, img, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Data{Text: text, img: img}, nil
|
||||
}
|
||||
|
||||
// NewCustomGenerator creates a new captcha based on a custom text generator.
|
||||
func NewCustomGenerator(
|
||||
width int, height int, generator func() (anwser string, question string), option ...SetOption,
|
||||
) (*Data, error) {
|
||||
options := newDefaultOption(width, height)
|
||||
for _, setOption := range option {
|
||||
setOption(options)
|
||||
}
|
||||
|
||||
answer, question := generator()
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
if err := drawWithOption(question, img, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Data{Text: answer, img: img}, nil
|
||||
}
|
||||
|
||||
func drawWithOption(text string, img *image.NRGBA, options *Options) error {
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{options.BackgroundColor}, image.Point{}, draw.Src)
|
||||
drawNoise(img, options)
|
||||
drawCurves(img, options)
|
||||
return drawText(text, img, options)
|
||||
}
|
||||
|
||||
func randomText(opts *Options) (text string) {
|
||||
n := len(opts.CharPreset)
|
||||
n := len([]rune(opts.CharPreset))
|
||||
for i := 0; i < opts.TextLength; i++ {
|
||||
text += string(opts.CharPreset[rng.Intn(n)])
|
||||
text += string([]rune(opts.CharPreset)[rand.Intn(n)])
|
||||
}
|
||||
|
||||
return text
|
||||
|
@ -178,16 +197,16 @@ func randomText(opts *Options) (text string) {
|
|||
func drawNoise(img *image.NRGBA, opts *Options) {
|
||||
noiseCount := (opts.width * opts.height) / int(28.0/opts.Noise)
|
||||
for i := 0; i < noiseCount; i++ {
|
||||
x := rng.Intn(opts.width)
|
||||
y := rng.Intn(opts.height)
|
||||
x := rand.Intn(opts.width)
|
||||
y := rand.Intn(opts.height)
|
||||
img.Set(x, y, randomColor())
|
||||
}
|
||||
}
|
||||
|
||||
func randomColor() color.RGBA {
|
||||
red := rng.Intn(256)
|
||||
green := rng.Intn(256)
|
||||
blue := rng.Intn(256)
|
||||
red := rand.Intn(256)
|
||||
green := rand.Intn(256)
|
||||
blue := rand.Intn(256)
|
||||
|
||||
return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: uint8(255)}
|
||||
}
|
||||
|
@ -205,14 +224,14 @@ func drawSineCurve(img *image.NRGBA, opts *Options) {
|
|||
if opts.width <= 40 {
|
||||
xStart, xEnd = 1, opts.width-1
|
||||
} else {
|
||||
xStart = rng.Intn(opts.width/10) + 1
|
||||
xEnd = opts.width - rng.Intn(opts.width/10) - 1
|
||||
xStart = rand.Intn(opts.width/10) + 1
|
||||
xEnd = opts.width - rand.Intn(opts.width/10) - 1
|
||||
}
|
||||
curveHeight := float64(rng.Intn(opts.height/6) + opts.height/6)
|
||||
yStart := rng.Intn(opts.height*2/3) + opts.height/6
|
||||
angle := 1.0 + rng.Float64()
|
||||
curveHeight := float64(rand.Intn(opts.height/6) + opts.height/6)
|
||||
yStart := rand.Intn(opts.height*2/3) + opts.height/6
|
||||
angle := 1.0 + rand.Float64()
|
||||
yFlip := 1.0
|
||||
if rng.Intn(2) == 0 {
|
||||
if rand.Intn(2) == 0 {
|
||||
yFlip = -1.0
|
||||
}
|
||||
curveColor := randomColorFromOptions(opts)
|
||||
|
@ -232,15 +251,15 @@ func drawText(text string, img *image.NRGBA, opts *Options) error {
|
|||
ctx.SetFont(ttfFont)
|
||||
|
||||
fontSpacing := opts.width / len(text)
|
||||
fontOffset := rng.Intn(fontSpacing / 2)
|
||||
fontOffset := rand.Intn(fontSpacing / 2)
|
||||
|
||||
for idx, char := range text {
|
||||
fontScale := 0.8 + rng.Float64()*0.4
|
||||
fontScale := 0.8 + rand.Float64()*0.4
|
||||
fontSize := float64(opts.height) / fontScale * opts.FontScale
|
||||
ctx.SetFontSize(fontSize)
|
||||
ctx.SetSrc(image.NewUniform(randomColorFromOptions(opts)))
|
||||
x := fontSpacing*idx + fontOffset
|
||||
y := opts.height/6 + rng.Intn(opts.height/3) + int(fontSize/2)
|
||||
y := opts.height/6 + rand.Intn(opts.height/3) + int(fontSize/2)
|
||||
pt := freetype.Pt(x, y)
|
||||
if _, err := ctx.DrawString(string(char), pt); err != nil {
|
||||
return err
|
||||
|
@ -256,19 +275,19 @@ func randomColorFromOptions(opts *Options) color.Color {
|
|||
return randomInvertColor(opts.BackgroundColor)
|
||||
}
|
||||
|
||||
return opts.Palette[rng.Intn(length)]
|
||||
return opts.Palette[rand.Intn(length)]
|
||||
}
|
||||
|
||||
func randomInvertColor(base color.Color) color.Color {
|
||||
baseLightness := getLightness(base)
|
||||
var value float64
|
||||
if baseLightness >= 0.5 {
|
||||
value = baseLightness - 0.3 - rng.Float64()*0.2
|
||||
value = baseLightness - 0.3 - rand.Float64()*0.2
|
||||
} else {
|
||||
value = baseLightness + 0.3 + rng.Float64()*0.2
|
||||
value = baseLightness + 0.3 + rand.Float64()*0.2
|
||||
}
|
||||
hue := float64(rng.Intn(361)) / 360
|
||||
saturation := 0.6 + rng.Float64()*0.2
|
||||
hue := float64(rand.Intn(361)) / 360
|
||||
saturation := 0.6 + rand.Float64()*0.2
|
||||
|
||||
return hsva{h: hue, s: saturation, v: value, a: 255}
|
||||
}
|
||||
|
@ -311,8 +330,8 @@ func minColor(numList ...uint32) (min uint32) {
|
|||
}
|
||||
|
||||
func randomEquation() (text string, equation string) {
|
||||
left := 1 + rng.Intn(9)
|
||||
right := 1 + rng.Intn(9)
|
||||
left := 1 + rand.Intn(9)
|
||||
right := 1 + rand.Intn(9)
|
||||
text = strconv.Itoa(left + right)
|
||||
equation = strconv.Itoa(left) + "+" + strconv.Itoa(right)
|
||||
|
||||
|
|
|
@ -69,6 +69,12 @@ func TestNewCaptchaOptions(t *testing.T) {
|
|||
NewMathExpr(100, 34, func(options *Options) {
|
||||
options.BackgroundColor = color.Black
|
||||
})
|
||||
|
||||
NewCustomGenerator(100, 34, func() (anwser string, question string) {
|
||||
return "4", "2x2?"
|
||||
}, func(o *Options) {
|
||||
o.BackgroundColor = color.Black
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewMathExpr(t *testing.T) {
|
||||
|
@ -78,7 +84,16 @@ func TestNewMathExpr(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCovNilFontError(t *testing.T) {
|
||||
func TestNewCustomGenerator(t *testing.T) {
|
||||
_, err := NewCustomGenerator(150, 50, func() (anwser string, question string) {
|
||||
return "1", "2"
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilFontError(t *testing.T) {
|
||||
temp := ttfFont
|
||||
ttfFont = nil
|
||||
|
||||
|
@ -92,6 +107,13 @@ func TestCovNilFontError(t *testing.T) {
|
|||
t.Fatal("Expect to get nil font error")
|
||||
}
|
||||
|
||||
_, err = NewCustomGenerator(150, 50, func() (anwser string, question string) {
|
||||
return "1", "2"
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Expect to get nil font error")
|
||||
}
|
||||
|
||||
ttfFont = temp
|
||||
}
|
||||
|
||||
|
@ -101,7 +123,7 @@ func (errReader) Read(_ []byte) (int, error) {
|
|||
return 0, errors.New("")
|
||||
}
|
||||
|
||||
func TestCovReaderErr(t *testing.T) {
|
||||
func TestReaderErr(t *testing.T) {
|
||||
err := LoadFontFromReader(errReader{})
|
||||
if err == nil {
|
||||
t.Fatal("Expect to get io.Reader error")
|
||||
|
|
|
@ -1 +1 @@
|
|||
[https://contributor-covenant.org/version/1/4/](https://contributor-covenant.org/version/1/4/)
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
|
7
example/basic/go.mod
Normal file
7
example/basic/go.mod
Normal file
|
@ -0,0 +1,7 @@
|
|||
module concord.hectabit.org/HectaBit/captcha/example/basic
|
||||
|
||||
go 1.12
|
||||
|
||||
replace concord.hectabit.org/HectaBit/captcha => ../../
|
||||
|
||||
require concord.hectabit.org/HectaBit/captcha v0.0.0-00010101000000-000000000000
|
5
example/basic/go.sum
Normal file
5
example/basic/go.sum
Normal file
|
@ -0,0 +1,5 @@
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec h1:arXJwtMuk5vqI1NHX0UTnNw977rYk5Sl4jQqHj+hun4=
|
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
10
example/load-font/go.mod
Normal file
10
example/load-font/go.mod
Normal file
|
@ -0,0 +1,10 @@
|
|||
module github.com/steambap/captcha/example/load-font
|
||||
|
||||
go 1.12
|
||||
|
||||
replace github.com/steambap/captcha => ../../
|
||||
|
||||
require (
|
||||
github.com/steambap/captcha v0.0.0-00010101000000-000000000000
|
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec
|
||||
)
|
5
example/load-font/go.sum
Normal file
5
example/load-font/go.sum
Normal file
|
@ -0,0 +1,5 @@
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec h1:arXJwtMuk5vqI1NHX0UTnNw977rYk5Sl4jQqHj+hun4=
|
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
40
fonts/gen.go
40
fonts/gen.go
|
@ -1,40 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// This program generates a go file for Comismsh font
|
||||
|
||||
func main() {
|
||||
src, err := ioutil.ReadFile("Comismsh.ttf")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprint(buf, "// DO NOT EDIT. This file is generated.\n\n")
|
||||
fmt.Fprint(buf, "package captcha\n\n")
|
||||
fmt.Fprint(buf, "// The following is Comismsh TrueType font data.\n")
|
||||
fmt.Fprint(buf, "var ttf = []byte{")
|
||||
for i, x := range src {
|
||||
if i&15 == 0 {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
fmt.Fprintf(buf, "%#02x,", x)
|
||||
}
|
||||
fmt.Fprint(buf, "\n}\n")
|
||||
|
||||
dst, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join("../", "font.go"), dst, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module concord.hectabit.org/HectaBit/captcha
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
golang.org/x/image v0.15.0
|
||||
)
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
Reference in a new issue