Compare commits
73 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 |
25 changed files with 516 additions and 5167 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 -race -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
|
||||
|
|
30
README.md
30
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,20 +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
|
||||
see [contributing.md](contributing.md) for more detail.
|
||||
|
||||
## License
|
||||
[MIT](LICENSE.md)
|
||||
[MIT](LICENSE)
|
||||
|
|
240
captcha.go
240
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,33 +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
|
||||
// LoadFont let you load an external font.
|
||||
func LoadFont(fontData []byte) error {
|
||||
var err error
|
||||
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 {
|
||||
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 {
|
||||
|
@ -91,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)}
|
||||
}
|
||||
|
@ -141,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
|
||||
|
@ -160,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
|
||||
|
@ -193,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
|
||||
}
|
||||
|
|
149
captcha_test.go
149
captcha_test.go
|
@ -2,9 +2,16 @@ package captcha
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
"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) {
|
||||
|
@ -13,7 +20,10 @@ func TestNewCaptcha(t *testing.T) {
|
|||
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) {
|
||||
|
@ -23,16 +33,67 @@ func TestSmallCaptcha(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
New(100, 34, func(options *Options) {
|
||||
options.BackgroundColor = color.Opaque
|
||||
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
|
||||
|
||||
|
@ -41,9 +102,34 @@ 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 {
|
||||
|
@ -55,3 +141,60 @@ func TestLoadFont(t *testing.T) {
|
|||
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 +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=
|
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=
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Captcha</title>
|
||||
<title>Load Font</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/captcha" alt="captcha">
|
|
@ -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=
|
36
history.md
36
history.md
|
@ -1,3 +1,39 @@
|
|||
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
|
||||
===================
|
||||
|
||||
|
|
Reference in a new issue