Compare commits
No commits in common. "main" and "v0.10.0" have entirely different histories.
25 changed files with 5170 additions and 519 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@
|
||||||
|
|
||||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||||
.glide/
|
.glide/
|
||||||
|
.idea/workspace.xml
|
||||||
|
|
9
.idea/captcha.iml
generated
Normal file
9
.idea/captcha.iml
generated
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?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
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?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
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
.travis.yml
Normal file
10
.travis.yml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 - 2024 Weilin Shi
|
Copyright (c) 2017 Weilin Shi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
30
README.md
30
README.md
|
@ -1,18 +1,17 @@
|
||||||
> Package captcha provides an easy to use, unopinionated API for captcha generation.
|
> Package captcha provides a simple API for captcha generation
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
[](https://pkg.go.dev/concord.hectabit.org/HectaBit/captcha)
|
[](https://godoc.org/github.com/steambap/captcha)
|
||||||
[](https://goreportcard.com/report/concord.hectabit.org/HectaBit/captcha)
|
[](https://travis-ci.org/steambap/captcha)
|
||||||
|
[](https://codecov.io/gh/steambap/captcha)
|
||||||
|
[](https://goreportcard.com/report/github.com/steambap/captcha)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Why another captcha generator?
|
|
||||||
Because I can.
|
|
||||||
|
|
||||||
## install
|
## install
|
||||||
```
|
```
|
||||||
go get concord.hectabit.org/HectaBit/captcha
|
go get github.com/steambap/captcha
|
||||||
```
|
```
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
@ -25,27 +24,20 @@ func handle(w http.ResponseWriter, r *http.Request) {
|
||||||
session.Values["captcha"] = data.Text
|
session.Values["captcha"] = data.Text
|
||||||
session.Save(r, w)
|
session.Save(r, w)
|
||||||
// send image data to client
|
// send image data to client
|
||||||
data.WriteImage(w)
|
data.WriteTo(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[documentation](https://pkg.go.dev/concord.hectabit.org/HectaBit/captcha) |
|
[documentation](https://godoc.org/github.com/steambap/captcha) |
|
||||||
[example](example/basic/main.go) |
|
[example](example/main.go)
|
||||||
[font example](example/load-font/main.go)
|
|
||||||
|
|
||||||
## sample image
|
## 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
|
## Contributing
|
||||||
If your found a bug, please contribute!
|
If your found a bug, please contribute!
|
||||||
see [contributing.md](contributing.md) for more detail.
|
see [contributing.md](contributing.md) for more detail
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](LICENSE)
|
[MIT](LICENSE.md)
|
||||||
|
|
244
captcha.go
244
captcha.go
|
@ -1,29 +1,23 @@
|
||||||
// Package captcha provides an easy to use, unopinionated API for captcha generation
|
// Package captcha provides a simple API for captcha generation
|
||||||
package captcha
|
package captcha
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed" // embed font
|
"github.com/golang/freetype"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/font"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"image/gif"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/freetype"
|
|
||||||
"github.com/golang/freetype/truetype"
|
|
||||||
"golang.org/x/image/font"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const charPreset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
const charPreset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
//go:embed fonts/Comismsh.ttf
|
var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
var ttf []byte
|
|
||||||
var ttfFont *truetype.Font
|
var ttfFont *truetype.Font
|
||||||
|
|
||||||
// Options manage captcha generation details.
|
// Options manage captcha generation details.
|
||||||
|
@ -41,18 +35,6 @@ type Options struct {
|
||||||
// CurveNumber is the number of curves to draw on captcha image.
|
// CurveNumber is the number of curves to draw on captcha image.
|
||||||
// It defaults to 2.
|
// It defaults to 2.
|
||||||
CurveNumber int
|
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
|
width int
|
||||||
height int
|
height int
|
||||||
|
@ -62,12 +44,8 @@ func newDefaultOption(width, height int) *Options {
|
||||||
return &Options{
|
return &Options{
|
||||||
BackgroundColor: color.Transparent,
|
BackgroundColor: color.Transparent,
|
||||||
CharPreset: charPreset,
|
CharPreset: charPreset,
|
||||||
TextLength: 6,
|
TextLength: 4,
|
||||||
CurveNumber: 2,
|
CurveNumber: 2,
|
||||||
FontDPI: 72.0,
|
|
||||||
FontScale: 1.0,
|
|
||||||
Noise: 1.0,
|
|
||||||
Palette: []color.Color{},
|
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
}
|
}
|
||||||
|
@ -78,56 +56,33 @@ type SetOption func(*Options)
|
||||||
|
|
||||||
// Data is the result of captcha generation.
|
// Data is the result of captcha generation.
|
||||||
// It has a `Text` field and a private `img` field that will
|
// It has a `Text` field and a private `img` field that will
|
||||||
// be used in `WriteImage` receiver.
|
// be used in `WriteTo` receiver
|
||||||
type Data struct {
|
type Data struct {
|
||||||
// Text is captcha solution.
|
// Text is captcha solution
|
||||||
Text string
|
Text string
|
||||||
|
|
||||||
img *image.NRGBA
|
img *image.NRGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteImage encodes image data and writes to an io.Writer.
|
// WriteTo encodes image data and writes to an io.Writer.
|
||||||
// It returns possible error from PNG encoding.
|
// It returns possible error from PNG encoding
|
||||||
func (data *Data) WriteImage(w io.Writer) error {
|
func (data *Data) WriteTo(w io.Writer) error {
|
||||||
return png.Encode(w, data.img)
|
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() {
|
func init() {
|
||||||
ttfFont, _ = freetype.ParseFont(ttf)
|
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 {
|
func LoadFont(fontData []byte) error {
|
||||||
var err error
|
var err error
|
||||||
ttfFont, err = freetype.ParseFont(fontData)
|
ttfFont, err = freetype.ParseFont(fontData)
|
||||||
return err
|
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.
|
// 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) {
|
func New(width int, height int, option ...SetOption) (*Data, error) {
|
||||||
options := newDefaultOption(width, height)
|
options := newDefaultOption(width, height)
|
||||||
for _, setOption := range option {
|
for _, setOption := range option {
|
||||||
|
@ -136,77 +91,39 @@ func New(width int, height int, option ...SetOption) (*Data, error) {
|
||||||
|
|
||||||
text := randomText(options)
|
text := randomText(options)
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||||
if err := drawWithOption(text, img, options); err != nil {
|
draw.Draw(img, img.Bounds(), &image.Uniform{options.BackgroundColor}, image.ZP, draw.Src)
|
||||||
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)
|
drawNoise(img, options)
|
||||||
drawCurves(img, options)
|
drawCurves(img, options)
|
||||||
return drawText(text, img, options)
|
err := drawText(text, img, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Data{Text: text, img: img}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomText(opts *Options) (text string) {
|
func randomText(opts *Options) (text string) {
|
||||||
n := len([]rune(opts.CharPreset))
|
n := len(opts.CharPreset)
|
||||||
for i := 0; i < opts.TextLength; i++ {
|
for i := 0; i < opts.TextLength; i++ {
|
||||||
text += string([]rune(opts.CharPreset)[rand.Intn(n)])
|
text += string(opts.CharPreset[rng.Intn(n)])
|
||||||
}
|
}
|
||||||
|
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawNoise(img *image.NRGBA, opts *Options) {
|
func drawNoise(img *image.NRGBA, opts *Options) {
|
||||||
noiseCount := (opts.width * opts.height) / int(28.0/opts.Noise)
|
noiseCount := (opts.width * opts.height) / 28
|
||||||
for i := 0; i < noiseCount; i++ {
|
for i := 0; i < noiseCount; i++ {
|
||||||
x := rand.Intn(opts.width)
|
x := rng.Intn(opts.width)
|
||||||
y := rand.Intn(opts.height)
|
y := rng.Intn(opts.height)
|
||||||
img.Set(x, y, randomColor())
|
img.Set(x, y, randomColor())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomColor() color.RGBA {
|
func randomColor() color.RGBA {
|
||||||
red := rand.Intn(256)
|
red := rng.Intn(255)
|
||||||
green := rand.Intn(256)
|
green := rng.Intn(255)
|
||||||
blue := rand.Intn(256)
|
blue := rng.Intn(255)
|
||||||
|
|
||||||
return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: uint8(255)}
|
return color.RGBA{R: uint8(red), G: uint8(green), B: uint8(blue), A: uint8(255)}
|
||||||
}
|
}
|
||||||
|
@ -224,17 +141,18 @@ func drawSineCurve(img *image.NRGBA, opts *Options) {
|
||||||
if opts.width <= 40 {
|
if opts.width <= 40 {
|
||||||
xStart, xEnd = 1, opts.width-1
|
xStart, xEnd = 1, opts.width-1
|
||||||
} else {
|
} else {
|
||||||
xStart = rand.Intn(opts.width/10) + 1
|
xStart = rng.Intn(opts.width/10) + 1
|
||||||
xEnd = opts.width - rand.Intn(opts.width/10) - 1
|
xEnd = opts.width - rng.Intn(opts.width/10) - 1
|
||||||
}
|
}
|
||||||
curveHeight := float64(rand.Intn(opts.height/6) + opts.height/6)
|
curveHeight := float64(rng.Intn(opts.height/6) + opts.height/6)
|
||||||
yStart := rand.Intn(opts.height*2/3) + opts.height/6
|
yStart := rng.Intn(opts.height*2/3) + opts.height/6
|
||||||
angle := 1.0 + rand.Float64()
|
angle := 1.0 + rng.Float64()
|
||||||
|
flip := rng.Intn(2) == 0
|
||||||
yFlip := 1.0
|
yFlip := 1.0
|
||||||
if rand.Intn(2) == 0 {
|
if flip {
|
||||||
yFlip = -1.0
|
yFlip = -1.0
|
||||||
}
|
}
|
||||||
curveColor := randomColorFromOptions(opts)
|
curveColor := randomDarkColor()
|
||||||
|
|
||||||
for x1 := xStart; x1 <= xEnd; x1++ {
|
for x1 := xStart; x1 <= xEnd; x1++ {
|
||||||
y := math.Sin(math.Pi*angle*float64(x1)/float64(opts.width)) * curveHeight * yFlip
|
y := math.Sin(math.Pi*angle*float64(x1)/float64(opts.width)) * curveHeight * yFlip
|
||||||
|
@ -242,24 +160,31 @@ 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 {
|
func drawText(text string, img *image.NRGBA, opts *Options) error {
|
||||||
ctx := freetype.NewContext()
|
ctx := freetype.NewContext()
|
||||||
ctx.SetDPI(opts.FontDPI)
|
ctx.SetDPI(92.0)
|
||||||
ctx.SetClip(img.Bounds())
|
ctx.SetClip(img.Bounds())
|
||||||
ctx.SetDst(img)
|
ctx.SetDst(img)
|
||||||
ctx.SetHinting(font.HintingFull)
|
ctx.SetHinting(font.HintingFull)
|
||||||
ctx.SetFont(ttfFont)
|
ctx.SetFont(ttfFont)
|
||||||
|
|
||||||
fontSpacing := opts.width / len(text)
|
fontSpacing := opts.width / len(text)
|
||||||
fontOffset := rand.Intn(fontSpacing / 2)
|
|
||||||
|
|
||||||
for idx, char := range text {
|
for idx, char := range text {
|
||||||
fontScale := 0.8 + rand.Float64()*0.4
|
fontScale := 1 + rng.Float64()*0.5
|
||||||
fontSize := float64(opts.height) / fontScale * opts.FontScale
|
fontSize := float64(opts.height) / fontScale
|
||||||
ctx.SetFontSize(fontSize)
|
ctx.SetFontSize(fontSize)
|
||||||
ctx.SetSrc(image.NewUniform(randomColorFromOptions(opts)))
|
ctx.SetSrc(image.NewUniform(randomDarkColor()))
|
||||||
x := fontSpacing*idx + fontOffset
|
x := fontSpacing*idx + fontSpacing/int(fontSize)
|
||||||
y := opts.height/6 + rand.Intn(opts.height/3) + int(fontSize/2)
|
y := opts.height/6 + rng.Intn(opts.height/3) + int(fontSize/2)
|
||||||
pt := freetype.Pt(x, y)
|
pt := freetype.Pt(x, y)
|
||||||
if _, err := ctx.DrawString(string(char), pt); err != nil {
|
if _, err := ctx.DrawString(string(char), pt); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -268,72 +193,3 @@ func drawText(text string, img *image.NRGBA, opts *Options) error {
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
151
captcha_test.go
151
captcha_test.go
|
@ -2,16 +2,9 @@ package captcha
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"image/color"
|
|
||||||
"image/color/palette"
|
|
||||||
"image/gif"
|
|
||||||
"image/jpeg"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/image/font/gofont/goregular"
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewCaptcha(t *testing.T) {
|
func TestNewCaptcha(t *testing.T) {
|
||||||
|
@ -20,10 +13,7 @@ func TestNewCaptcha(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err = data.WriteImage(buf)
|
data.WriteTo(buf)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmallCaptcha(t *testing.T) {
|
func TestSmallCaptcha(t *testing.T) {
|
||||||
|
@ -33,67 +23,16 @@ 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) {
|
func TestNewCaptchaOptions(t *testing.T) {
|
||||||
New(100, 34, func(options *Options) {
|
New(100, 34, func(options *Options) {
|
||||||
options.BackgroundColor = color.Opaque
|
options.BackgroundColor = color.Opaque
|
||||||
options.CharPreset = "1234567890"
|
options.CharPreset = "1234567890"
|
||||||
options.CurveNumber = 0
|
options.CurveNumber = 0
|
||||||
options.TextLength = 6
|
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 TestNewMathExpr(t *testing.T) {
|
func TestCovNilFontError(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
|
temp := ttfFont
|
||||||
ttfFont = nil
|
ttfFont = nil
|
||||||
|
|
||||||
|
@ -102,34 +41,9 @@ func TestNilFontError(t *testing.T) {
|
||||||
t.Fatal("Expect to get nil font error")
|
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
|
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) {
|
func TestLoadFont(t *testing.T) {
|
||||||
err := LoadFont(goregular.TTF)
|
err := LoadFont(goregular.TTF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -141,60 +55,3 @@ func TestLoadFont(t *testing.T) {
|
||||||
t.Fatal("LoadFont incorrectly parse an invalid font")
|
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://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
|
[https://contributor-covenant.org/version/1/4/](https://contributor-covenant.org/version/1/4/)
|
|
@ -1,7 +0,0 @@
|
||||||
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
|
|
|
@ -1,5 +0,0 @@
|
||||||
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,11 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,49 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.4 KiB |
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Load Font</title>
|
<title>Captcha</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<img src="/captcha" alt="captcha">
|
<img src="/captcha" alt="captcha">
|
|
@ -1,10 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,5 +0,0 @@
|
||||||
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,23 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/steambap/captcha"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/steambap/captcha"
|
|
||||||
"golang.org/x/image/font/gofont/goregular"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := captcha.LoadFont(goregular.TTF)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.HandleFunc("/", indexHandle)
|
http.HandleFunc("/", indexHandle)
|
||||||
http.HandleFunc("/captcha", captchaHandle)
|
http.HandleFunc("/captcha", captchaHandle)
|
||||||
fmt.Println("Server start at port 8080")
|
fmt.Println("Server start at port 8080")
|
||||||
err = http.ListenAndServe(":8080", nil)
|
err := http.ListenAndServe(":8080", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -34,13 +27,11 @@ func indexHandle(w http.ResponseWriter, _ *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func captchaHandle(w http.ResponseWriter, _ *http.Request) {
|
func captchaHandle(w http.ResponseWriter, _ *http.Request) {
|
||||||
img, err := captcha.New(150, 50, func(options *captcha.Options) {
|
img, err := captcha.New(150, 50)
|
||||||
options.FontScale = 0.8
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprint(w, nil)
|
fmt.Fprint(w, nil)
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
img.WriteImage(w)
|
img.WriteTo(w)
|
||||||
}
|
}
|
40
fonts/gen.go
Normal file
40
fonts/gen.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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
8
go.mod
|
@ -1,8 +0,0 @@
|
||||||
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
4
go.sum
|
@ -1,4 +0,0 @@
|
||||||
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,39 +1,3 @@
|
||||||
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
|
0.9.0 / 2017-09-20
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
Reference in a new issue