245 lines
5.7 KiB
Go
245 lines
5.7 KiB
Go
|
// Copyright 2016 Minho. All rights reserved.
|
||
|
// Copyright 2021 Flamego. All rights reserved.
|
||
|
// Use of this source code is governed by a MIT-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package captchautil
|
||
|
|
||
|
import (
|
||
|
"image"
|
||
|
"image/color"
|
||
|
"image/draw"
|
||
|
"image/jpeg"
|
||
|
"io"
|
||
|
"math"
|
||
|
"math/rand"
|
||
|
"time"
|
||
|
|
||
|
"github.com/golang/freetype"
|
||
|
"github.com/pkg/errors"
|
||
|
"golang.org/x/image/font"
|
||
|
)
|
||
|
|
||
|
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
|
||
|
type Complexity int
|
||
|
|
||
|
const (
|
||
|
ComplexityLow Complexity = iota
|
||
|
ComplexityMedium
|
||
|
ComplexityHigh
|
||
|
)
|
||
|
|
||
|
// Image is a captcha image.
|
||
|
type Image struct {
|
||
|
width int
|
||
|
height int
|
||
|
dpi float64
|
||
|
nrgba *image.NRGBA
|
||
|
}
|
||
|
|
||
|
type Option func(*Image) error
|
||
|
|
||
|
// CreateImage creates and returns a new image with given width, height, DPI,
|
||
|
// background color and additional options.
|
||
|
func CreateImage(width, height, dpi int, bgColor color.RGBA, opts ...Option) (*Image, error) {
|
||
|
nrgba := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||
|
draw.Draw(nrgba, nrgba.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src)
|
||
|
|
||
|
img := &Image{
|
||
|
height: height,
|
||
|
width: width,
|
||
|
dpi: float64(dpi),
|
||
|
nrgba: nrgba,
|
||
|
}
|
||
|
|
||
|
for _, opt := range opts {
|
||
|
err := opt(img)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
return img, nil
|
||
|
}
|
||
|
|
||
|
// Encode encodes the final JPEG image to the given writer.
|
||
|
func (img *Image) Encode(w io.Writer) error {
|
||
|
return jpeg.Encode(w, img.nrgba, &jpeg.Options{Quality: 100})
|
||
|
}
|
||
|
|
||
|
// Noise draws background noise on the image.
|
||
|
func Noise(complex Complexity) Option {
|
||
|
return func(img *Image) error {
|
||
|
density := 18
|
||
|
if complex == ComplexityLow {
|
||
|
density = 28
|
||
|
} else if complex == ComplexityMedium {
|
||
|
density = 18
|
||
|
} else if complex == ComplexityHigh {
|
||
|
density = 8
|
||
|
}
|
||
|
maxSize := (img.height * img.width) / density
|
||
|
for i := 0; i < maxSize; i++ {
|
||
|
rw := r.Intn(img.width)
|
||
|
rh := r.Intn(img.height)
|
||
|
img.nrgba.Set(rw, rh, randomColor())
|
||
|
size := r.Intn(maxSize)
|
||
|
if size%3 == 0 {
|
||
|
img.nrgba.Set(rw+1, rh+1, randomColor())
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TextNoise draws background text noise on the image.
|
||
|
func TextNoise(complex Complexity) Option {
|
||
|
return func(img *Image) error {
|
||
|
density := 1500
|
||
|
if complex == ComplexityLow {
|
||
|
density = 2000
|
||
|
} else if complex == ComplexityMedium {
|
||
|
density = 1500
|
||
|
} else if complex == ComplexityHigh {
|
||
|
density = 1000
|
||
|
}
|
||
|
maxSize := (img.height * img.width) / density
|
||
|
|
||
|
c := freetype.NewContext()
|
||
|
c.SetDPI(img.dpi)
|
||
|
c.SetClip(img.nrgba.Bounds())
|
||
|
c.SetDst(img.nrgba)
|
||
|
c.SetHinting(font.HintingFull)
|
||
|
rawFontSize := float64(img.height) / (1 + float64(r.Intn(7))/float64(10))
|
||
|
|
||
|
for i := 0; i < maxSize; i++ {
|
||
|
text, err := RandomText(1)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "TextNoise: get random text")
|
||
|
}
|
||
|
fontSize := rawFontSize/2 + float64(r.Intn(5))
|
||
|
|
||
|
c.SetSrc(image.NewUniform(RandomLightColor()))
|
||
|
c.SetFontSize(fontSize)
|
||
|
|
||
|
f, err := randFontFamily()
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "TextNoise: get random font family")
|
||
|
}
|
||
|
c.SetFont(f)
|
||
|
|
||
|
rw := r.Intn(img.width)
|
||
|
rh := r.Intn(img.height)
|
||
|
pt := freetype.Pt(rw, rh)
|
||
|
|
||
|
_, err = c.DrawString(text, pt)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "TextNoise: draw string")
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Text draws text on the image.
|
||
|
func Text(text string) Option {
|
||
|
return func(img *Image) error {
|
||
|
c := freetype.NewContext()
|
||
|
c.SetDPI(img.dpi)
|
||
|
c.SetClip(img.nrgba.Bounds())
|
||
|
c.SetDst(img.nrgba)
|
||
|
c.SetHinting(font.HintingFull)
|
||
|
|
||
|
fontWidth := img.width / len(text)
|
||
|
for i, s := range text {
|
||
|
fontSize := float64(img.height) / (1 + float64(r.Intn(7))/float64(9))
|
||
|
c.SetSrc(image.NewUniform(randomDeepColor()))
|
||
|
c.SetFontSize(fontSize)
|
||
|
|
||
|
f, err := randFontFamily()
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "Text: get random font family")
|
||
|
}
|
||
|
c.SetFont(f)
|
||
|
|
||
|
x := (fontWidth)*i + (fontWidth)/int(fontSize)
|
||
|
y := 5 + r.Intn(img.height/2) + int(fontSize/2)
|
||
|
pt := freetype.Pt(x, y)
|
||
|
|
||
|
_, err = c.DrawString(string(s), pt)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "Text: draw string")
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Border draws border on the image.
|
||
|
func Border(color color.RGBA) Option {
|
||
|
return func(img *Image) error {
|
||
|
for x := 0; x < img.width; x++ {
|
||
|
img.nrgba.Set(x, 0, color)
|
||
|
img.nrgba.Set(x, img.height-1, color)
|
||
|
}
|
||
|
for y := 0; y < img.height; y++ {
|
||
|
img.nrgba.Set(0, y, color)
|
||
|
img.nrgba.Set(img.width-1, y, color)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Curve draws a curve on the image.
|
||
|
func Curve() Option {
|
||
|
return func(img *Image) error {
|
||
|
random := func(min, max int64) float64 {
|
||
|
decimal := rand.Float64()
|
||
|
if max <= 0 {
|
||
|
return (float64(rand.Int63n((min*-1)-(max*-1))+(max*-1)) + decimal) * -1
|
||
|
}
|
||
|
if min < 0 && max > 0 {
|
||
|
if rand.Int()%2 == 0 {
|
||
|
return float64(rand.Int63n(max)) + decimal
|
||
|
} else {
|
||
|
return (float64(rand.Int63n(min*-1)) + decimal) * -1
|
||
|
}
|
||
|
}
|
||
|
return float64(rand.Int63n(max-min)+min) + decimal
|
||
|
}
|
||
|
|
||
|
a := r.Intn(img.height / 2) // Amplitude
|
||
|
b := random(int64(-img.height/4), int64(img.height/4)) // Y-axis offset
|
||
|
f := random(int64(-img.height/4), int64(img.height/4)) // X-axis offset
|
||
|
// Period
|
||
|
var t float64
|
||
|
if img.height > img.width/2 {
|
||
|
t = random(int64(img.width/2), int64(img.height))
|
||
|
} else {
|
||
|
t = random(int64(img.height), int64(img.width/2))
|
||
|
}
|
||
|
w := (2 * math.Pi) / t
|
||
|
|
||
|
end := int(random(int64(float64(img.width)*0.8), int64(img.width)))
|
||
|
c := color.RGBA{
|
||
|
R: uint8(r.Intn(150)),
|
||
|
G: uint8(r.Intn(150)),
|
||
|
B: uint8(r.Intn(150)),
|
||
|
A: uint8(255),
|
||
|
}
|
||
|
|
||
|
py := float64(0)
|
||
|
for px := 0; px < end; px++ {
|
||
|
if w != 0 {
|
||
|
py = float64(a)*math.Sin(w*float64(px)+f) + b + (float64(img.width) / float64(5))
|
||
|
i := img.height / 5
|
||
|
for i > 0 {
|
||
|
img.nrgba.Set(px+i, int(py), c)
|
||
|
i--
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|