Compare commits
77 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 | ||
|
56e03474ba | ||
|
891b52d957 | ||
|
1b36f64f5d | ||
|
fc4a5d0dfc | ||
|
1284662a43 | ||
|
b204ba9578 | ||
|
d13db8c2b6 | ||
|
6a043360ff | ||
|
6cc73c97f7 | ||
|
8bb5ffd35d | ||
|
57d17d0ed4 | ||
|
e5ca1c346e | ||
|
10262641db | ||
|
abab5c281c | ||
|
d0ec38c405 | ||
|
fdf3057743 | ||
|
98d9d078e2 | ||
|
dbb96819ea | ||
|
3e4b61d8c9 | ||
|
b25acadd0a | ||
|
32fccd9c5c | ||
|
6290b9a7ef | ||
|
5ef8d99a13 | ||
|
02daf62bc0 | ||
|
f9832124ae | ||
|
551c5cd7fb | ||
|
2374b8a839 | ||
|
c0bf080544 | ||
|
66c7cdc2f4 | ||
|
21cc8dcbcb | ||
|
29b89c3e2a |
26 changed files with 563 additions and 5166 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,4 +12,3 @@
|
|||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
.idea/workspace.xml
|
||||
|
|
9
.idea/captcha.iml
generated
9
.idea/captcha.iml
generated
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/captcha.iml" filepath="$PROJECT_DIR$/.idea/captcha.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
10
.travis.yml
10
.travis.yml
|
@ -1,10 +0,0 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
|
||||
script:
|
||||
- go test -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 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
|
||||
|
|
32
README.md
32
README.md
|
@ -1,17 +1,18 @@
|
|||
> Package captcha provides a simple API for captcha generation
|
||||
> Package captcha provides an easy to use, unopinionated API for captcha generation.
|
||||
|
||||
<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?
|
||||
Because I can.
|
||||
|
||||
## install
|
||||
```
|
||||
go get github.com/steambap/captcha
|
||||
go get concord.hectabit.org/HectaBit/captcha
|
||||
```
|
||||
|
||||
## usage
|
||||
|
@ -24,16 +25,27 @@ func handle(w http.ResponseWriter, r *http.Request) {
|
|||
session.Values["captcha"] = data.Text
|
||||
session.Save(r, w)
|
||||
// send image data to client
|
||||
data.WriteTo(w)
|
||||
data.WriteImage(w)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
[documentation](https://godoc.org/github.com/steambap/captcha) |
|
||||
[example](example/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)
|
||||
|
|
245
captcha.go
245
captcha.go
|
@ -1,23 +1,29 @@
|
|||
// Package captcha provides a simple API for captcha generation
|
||||
// Package captcha provides an easy to use, unopinionated API for captcha generation
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/font"
|
||||
_ "embed" // embed font
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/font"
|
||||
)
|
||||
|
||||
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.
|
||||
|
@ -35,6 +41,18 @@ type Options struct {
|
|||
// CurveNumber is the number of curves to draw on captcha image.
|
||||
// It defaults to 2.
|
||||
CurveNumber int
|
||||
// FontDPI controls DPI (dots per inch) of font.
|
||||
// The default is 72.0.
|
||||
FontDPI float64
|
||||
// FontScale controls the scale of font.
|
||||
// The default is 1.0.
|
||||
FontScale float64
|
||||
// Noise controls the number of noise drawn.
|
||||
// A noise dot is drawn for every 28 pixel by default.
|
||||
// The default is 1.0.
|
||||
Noise float64
|
||||
// Palette is the set of colors to chose from
|
||||
Palette color.Palette
|
||||
|
||||
width int
|
||||
height int
|
||||
|
@ -44,8 +62,12 @@ 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,
|
||||
Noise: 1.0,
|
||||
Palette: []color.Color{},
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
|
@ -56,30 +78,56 @@ type SetOption func(*Options)
|
|||
|
||||
// Data is the result of captcha generation.
|
||||
// It has a `Text` field and a private `img` field that will
|
||||
// be used in `WriteTo` receiver
|
||||
// be used in `WriteImage` receiver.
|
||||
type Data struct {
|
||||
// Text is captcha solution
|
||||
// Text is captcha solution.
|
||||
Text string
|
||||
|
||||
img *image.NRGBA
|
||||
}
|
||||
|
||||
// WriteTo encodes image data and writes to an io.Writer.
|
||||
// It returns possible error from PNG encoding
|
||||
func (data *Data) WriteTo(w io.Writer) error {
|
||||
// WriteImage encodes image data and writes to an io.Writer.
|
||||
// It returns possible error from PNG encoding.
|
||||
func (data *Data) WriteImage(w io.Writer) error {
|
||||
return png.Encode(w, data.img)
|
||||
}
|
||||
|
||||
// WriteJPG encodes image data in JPEG format and writes to an io.Writer.
|
||||
// It returns possible error from JPEG encoding.
|
||||
func (data *Data) WriteJPG(w io.Writer, o *jpeg.Options) error {
|
||||
return jpeg.Encode(w, data.img, o)
|
||||
}
|
||||
|
||||
// WriteGIF encodes image data in GIF format and writes to an io.Writer.
|
||||
// It returns possible error from GIF encoding.
|
||||
func (data *Data) WriteGIF(w io.Writer, o *gif.Options) error {
|
||||
return gif.Encode(w, data.img, o)
|
||||
}
|
||||
|
||||
func init() {
|
||||
ttfFont, _ = freetype.ParseFont(ttf)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// LoadFont let you load an external font.
|
||||
func LoadFont(fontData []byte) error {
|
||||
var err error
|
||||
ttfFont, err = freetype.ParseFont(ttf)
|
||||
ttfFont, err = freetype.ParseFont(fontData)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadFontFromReader load an external font from an io.Reader interface.
|
||||
func LoadFontFromReader(reader io.Reader) error {
|
||||
b, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return LoadFont(b)
|
||||
}
|
||||
|
||||
// New creates a new captcha.
|
||||
// It returns captcha data and any freetype drawing error encountered
|
||||
// It returns captcha data and any freetype drawing error encountered.
|
||||
func New(width int, height int, option ...SetOption) (*Data, error) {
|
||||
options := newDefaultOption(width, height)
|
||||
for _, setOption := range option {
|
||||
|
@ -88,39 +136,77 @@ 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
|
||||
}
|
||||
|
||||
return &Data{Text: text, img: img}, nil
|
||||
}
|
||||
|
||||
// NewMathExpr creates a new captcha.
|
||||
// It will generate a image with a math expression like `1 + 2`.
|
||||
func NewMathExpr(width int, height int, option ...SetOption) (*Data, error) {
|
||||
options := newDefaultOption(width, height)
|
||||
for _, setOption := range option {
|
||||
setOption(options)
|
||||
}
|
||||
|
||||
text, equation := randomEquation()
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
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
|
||||
}
|
||||
|
||||
func drawNoise(img *image.NRGBA, opts *Options) {
|
||||
noiseCount := (opts.width * opts.height) / 28
|
||||
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(255)
|
||||
green := rng.Intn(255)
|
||||
blue := rng.Intn(255)
|
||||
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)}
|
||||
}
|
||||
|
@ -138,18 +224,17 @@ 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()
|
||||
flip := rng.Intn(2) == 0
|
||||
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 flip {
|
||||
if rand.Intn(2) == 0 {
|
||||
yFlip = -1.0
|
||||
}
|
||||
curveColor := randomDarkColor()
|
||||
curveColor := randomColorFromOptions(opts)
|
||||
|
||||
for x1 := xStart; x1 <= xEnd; x1++ {
|
||||
y := math.Sin(math.Pi*angle*float64(x1)/float64(opts.width)) * curveHeight * yFlip
|
||||
|
@ -157,31 +242,24 @@ func drawSineCurve(img *image.NRGBA, opts *Options) {
|
|||
}
|
||||
}
|
||||
|
||||
func randomDarkColor() hsva {
|
||||
hue := float64(rng.Intn(361)) / 360
|
||||
saturation := 0.6 + rng.Float64()*0.2
|
||||
value := 0.25 + rng.Float64()*0.2
|
||||
|
||||
return hsva{h: hue, s: saturation, v: value, a: uint8(255)}
|
||||
}
|
||||
|
||||
func drawText(text string, img *image.NRGBA, opts *Options) error {
|
||||
ctx := freetype.NewContext()
|
||||
ctx.SetDPI(92.0)
|
||||
ctx.SetDPI(opts.FontDPI)
|
||||
ctx.SetClip(img.Bounds())
|
||||
ctx.SetDst(img)
|
||||
ctx.SetHinting(font.HintingFull)
|
||||
ctx.SetFont(ttfFont)
|
||||
|
||||
fontSpacing := opts.width / len(text)
|
||||
fontOffset := rand.Intn(fontSpacing / 2)
|
||||
|
||||
for idx, char := range text {
|
||||
fontScale := 1 + rng.Float64()*0.5
|
||||
fontSize := float64(opts.height) / fontScale
|
||||
fontScale := 0.8 + rand.Float64()*0.4
|
||||
fontSize := float64(opts.height) / fontScale * opts.FontScale
|
||||
ctx.SetFontSize(fontSize)
|
||||
ctx.SetSrc(image.NewUniform(randomDarkColor()))
|
||||
x := fontSpacing*idx + fontSpacing/int(fontSize)
|
||||
y := opts.height/6 + rng.Intn(opts.height/3) + int(fontSize/2)
|
||||
ctx.SetSrc(image.NewUniform(randomColorFromOptions(opts)))
|
||||
x := fontSpacing*idx + fontOffset
|
||||
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
|
||||
|
@ -190,3 +268,72 @@ func drawText(text string, img *image.NRGBA, opts *Options) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomColorFromOptions(opts *Options) color.Color {
|
||||
length := len(opts.Palette)
|
||||
if length == 0 {
|
||||
return randomInvertColor(opts.BackgroundColor)
|
||||
}
|
||||
|
||||
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 - rand.Float64()*0.2
|
||||
} else {
|
||||
value = baseLightness + 0.3 + rand.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}
|
||||
}
|
||||
|
||||
func getLightness(colour color.Color) float64 {
|
||||
r, g, b, a := colour.RGBA()
|
||||
// transparent
|
||||
if a == 0 {
|
||||
return 1.0
|
||||
}
|
||||
max := maxColor(r, g, b)
|
||||
min := minColor(r, g, b)
|
||||
|
||||
l := (float64(max) + float64(min)) / (2 * 255)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func maxColor(numList ...uint32) (max uint32) {
|
||||
for _, num := range numList {
|
||||
colorVal := num & 255
|
||||
if colorVal > max {
|
||||
max = colorVal
|
||||
}
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
||||
|
||||
func minColor(numList ...uint32) (min uint32) {
|
||||
min = 255
|
||||
for _, num := range numList {
|
||||
colorVal := num & 255
|
||||
if colorVal < min {
|
||||
min = colorVal
|
||||
}
|
||||
}
|
||||
|
||||
return min
|
||||
}
|
||||
|
||||
func randomEquation() (text string, equation string) {
|
||||
left := 1 + rand.Intn(9)
|
||||
right := 1 + rand.Intn(9)
|
||||
text = strconv.Itoa(left + right)
|
||||
equation = strconv.Itoa(left) + "+" + strconv.Itoa(right)
|
||||
|
||||
return text, equation
|
||||
}
|
||||
|
|
168
captcha_test.go
168
captcha_test.go
|
@ -2,18 +2,59 @@ package captcha
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"image/color"
|
||||
"image/color/palette"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
)
|
||||
|
||||
func TestNewCaptcha(t *testing.T) {
|
||||
New(36, 12)
|
||||
data, err := New(150, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
data.WriteTo(buf)
|
||||
err = data.WriteImage(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmallCaptcha(t *testing.T) {
|
||||
_, err := New(36, 12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeJPG(t *testing.T) {
|
||||
data, err := New(150, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
err = data.WriteJPG(buf, &jpeg.Options{Quality: 70})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeGIF(t *testing.T) {
|
||||
data, err := New(150, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
err = data.WriteGIF(buf, &gif.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCaptchaOptions(t *testing.T) {
|
||||
|
@ -22,10 +63,37 @@ func TestNewCaptchaOptions(t *testing.T) {
|
|||
options.CharPreset = "1234567890"
|
||||
options.CurveNumber = 0
|
||||
options.TextLength = 6
|
||||
options.Palette = palette.WebSafe
|
||||
})
|
||||
|
||||
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 TestCovNilFontError(t *testing.T) {
|
||||
func TestNewMathExpr(t *testing.T) {
|
||||
_, err := NewMathExpr(150, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
@ -34,5 +102,99 @@ func TestCovNilFontError(t *testing.T) {
|
|||
t.Fatal("Expect to get nil font error")
|
||||
}
|
||||
|
||||
_, err = NewMathExpr(150, 50)
|
||||
if err == nil {
|
||||
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
|
||||
}
|
||||
|
||||
type errReader struct{}
|
||||
|
||||
func (errReader) Read(_ []byte) (int, error) {
|
||||
return 0, errors.New("")
|
||||
}
|
||||
|
||||
func TestReaderErr(t *testing.T) {
|
||||
err := LoadFontFromReader(errReader{})
|
||||
if err == nil {
|
||||
t.Fatal("Expect to get io.Reader error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFont(t *testing.T) {
|
||||
err := LoadFont(goregular.TTF)
|
||||
if err != nil {
|
||||
t.Fatal("Fail to load go font")
|
||||
}
|
||||
|
||||
err = LoadFont([]byte("invalid"))
|
||||
if err == nil {
|
||||
t.Fatal("LoadFont incorrectly parse an invalid font")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFontFromReader(t *testing.T) {
|
||||
file, err := os.Open("./fonts/Comismsh.ttf")
|
||||
if err != nil {
|
||||
t.Fatal("Fail to load test file")
|
||||
}
|
||||
|
||||
if err = LoadFontFromReader(file); err != nil {
|
||||
t.Fatal("Fail to load font from io.Reader")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxColor(t *testing.T) {
|
||||
var result uint32
|
||||
result = maxColor()
|
||||
if result != 0 {
|
||||
t.Fatalf("Expect max color to be 0, got %v", result)
|
||||
}
|
||||
result = maxColor(1)
|
||||
if result != 1 {
|
||||
t.Fatalf("Expect max color to be 1, got %v", result)
|
||||
}
|
||||
result = maxColor(52428, 65535)
|
||||
if result != 255 {
|
||||
t.Fatalf("Expect max color to be 255, got %v", result)
|
||||
}
|
||||
var rng = rand.New(rand.NewSource(0))
|
||||
for i := 0; i < 10; i++ {
|
||||
result = maxColor(rng.Uint32(), rng.Uint32(), rng.Uint32())
|
||||
if result > 255 {
|
||||
t.Fatalf("Number out of range: %v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinColor(t *testing.T) {
|
||||
var result uint32
|
||||
result = minColor()
|
||||
if result != 255 {
|
||||
t.Fatalf("Expect min color to be 255, got %v", result)
|
||||
}
|
||||
result = minColor(1)
|
||||
if result != 1 {
|
||||
t.Fatalf("Expect min color to be 1, got %v", result)
|
||||
}
|
||||
result = minColor(52428, 65535)
|
||||
if result != 204 {
|
||||
t.Fatalf("Expect min color to be 1, got %v", result)
|
||||
}
|
||||
var rng = rand.New(rand.NewSource(0))
|
||||
for i := 0; i < 10; i++ {
|
||||
result = minColor(rng.Uint32(), rng.Uint32(), rng.Uint32())
|
||||
if result > 255 {
|
||||
t.Fatalf("Number out of range: %v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1
code-of-conduct.md
Normal file
1
code-of-conduct.md
Normal file
|
@ -0,0 +1 @@
|
|||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
|
17
contributing.md
Normal file
17
contributing.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
First off, thank you for considering contributing to captcha. It's people like you that make captcha such a great module.
|
||||
|
||||
Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.
|
||||
|
||||
Captcha is an open source project and we love to receive contributions from our community — you! There are many ways to contribute, from writing blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into captcha itself.
|
||||
|
||||
Step your can follow to start:
|
||||
1. Fork it!
|
||||
2. Create your feature branch: `git checkout -b my-new-feature`
|
||||
3. Commit your changes: `git commit -am 'Add some feature'`
|
||||
4. Push to the branch: `git push origin my-new-feature`
|
||||
5. Submit a pull request :D
|
||||
|
||||
Ground Rules:
|
||||
- Ensure all tests pass.
|
||||
- Run `go fmt` before commit. Event better, make go report happy.
|
||||
- Make sure you only implement ONE feature or bugfix in a pull request. Do not split one line fix in multiple commits to confuse others either.
|
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=
|
11
example/basic/index.html
Normal file
11
example/basic/index.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Captcha</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/captcha-default" alt="captcha">
|
||||
<img src="/captcha-math" alt="captcha">
|
||||
</body>
|
||||
</html>
|
49
example/basic/main.go
Normal file
49
example/basic/main.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/steambap/captcha"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", indexHandle)
|
||||
http.HandleFunc("/captcha-default", captchaHandle)
|
||||
http.HandleFunc("/captcha-math", mathHandle)
|
||||
fmt.Println("Server start at port 8080")
|
||||
err := http.ListenAndServe(":8080", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandle(w http.ResponseWriter, _ *http.Request) {
|
||||
doc, err := template.ParseFiles("index.html")
|
||||
if err != nil {
|
||||
fmt.Fprint(w, err.Error())
|
||||
return
|
||||
}
|
||||
doc.Execute(w, nil)
|
||||
}
|
||||
|
||||
func captchaHandle(w http.ResponseWriter, _ *http.Request) {
|
||||
img, err := captcha.New(150, 50)
|
||||
if err != nil {
|
||||
fmt.Fprint(w, nil)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
img.WriteImage(w)
|
||||
}
|
||||
|
||||
func mathHandle(w http.ResponseWriter, _ *http.Request) {
|
||||
img, err := captcha.NewMathExpr(150, 50)
|
||||
if err != nil {
|
||||
fmt.Fprint(w, nil)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
img.WriteImage(w)
|
||||
}
|
BIN
example/captcha-math.png
Normal file
BIN
example/captcha-math.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
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=
|
|
@ -1,10 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Captcha</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/captcha" alt="captcha">
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Load Font</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/captcha" alt="captcha">
|
||||
</body>
|
||||
</html>
|
|
@ -2,16 +2,23 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/steambap/captcha"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/steambap/captcha"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := captcha.LoadFont(goregular.TTF)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/", indexHandle)
|
||||
http.HandleFunc("/captcha", captchaHandle)
|
||||
fmt.Println("Server start at port 8080")
|
||||
err := http.ListenAndServe(":8080", nil)
|
||||
err = http.ListenAndServe(":8080", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -27,11 +34,13 @@ func indexHandle(w http.ResponseWriter, _ *http.Request) {
|
|||
}
|
||||
|
||||
func captchaHandle(w http.ResponseWriter, _ *http.Request) {
|
||||
img, err := captcha.New(150, 50)
|
||||
img, err := captcha.New(150, 50, func(options *captcha.Options) {
|
||||
options.FontScale = 0.8
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprint(w, nil)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
img.WriteTo(w)
|
||||
img.WriteImage(w)
|
||||
}
|
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=
|
40
history.md
Normal file
40
history.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
1.3.0 / 2018-11-7
|
||||
===================
|
||||
|
||||
* Add Palette option
|
||||
|
||||
1.2.0 / 2017-12-26
|
||||
===================
|
||||
|
||||
* Add Noise option
|
||||
|
||||
1.1.0 / 2017-11-16
|
||||
===================
|
||||
|
||||
* Add WriteJPG and WriteGIF API
|
||||
|
||||
1.0.0 / 2017-10-10
|
||||
===================
|
||||
|
||||
* Add LoadFontFromReader API
|
||||
* Rename WriteTo to WriteImage
|
||||
|
||||
0.12.0 / 2017-10-07
|
||||
===================
|
||||
|
||||
* Add FontDPI and FontScale options
|
||||
|
||||
0.11.0 / 2017-09-28
|
||||
===================
|
||||
|
||||
* Add NewMathExpr API
|
||||
|
||||
0.10.0 / 2017-09-23
|
||||
===================
|
||||
|
||||
* Add LoadFont API
|
||||
|
||||
0.9.0 / 2017-09-20
|
||||
===================
|
||||
|
||||
* Initial release
|
Reference in a new issue