mirror of
https://github.com/wenlng/go-captcha.git
synced 2025-09-27 12:22:12 +08:00
400 lines
9.9 KiB
Go
400 lines
9.9 KiB
Go
/**
|
|
* @Author Awen
|
|
* @Date 2024/06/01
|
|
* @Email wengaolng@gmail.com
|
|
**/
|
|
|
|
package slide
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"math"
|
|
|
|
"github.com/wenlng/go-captcha/v2/base/helper"
|
|
"github.com/wenlng/go-captcha/v2/base/imagedata"
|
|
"github.com/wenlng/go-captcha/v2/base/logger"
|
|
"github.com/wenlng/go-captcha/v2/base/option"
|
|
"github.com/wenlng/go-captcha/v2/base/randgen"
|
|
"github.com/wenlng/go-captcha/v2/base/random"
|
|
)
|
|
|
|
type Mode int
|
|
|
|
const (
|
|
ModeBasic Mode = iota
|
|
ModeDrag
|
|
)
|
|
|
|
// Captcha defines the interface for slide CAPTCHA
|
|
type Captcha interface {
|
|
setOptions(opts ...Option)
|
|
setResources(resources ...Resource)
|
|
GetOptions() *Options
|
|
Generate() (CaptchaData, error)
|
|
}
|
|
|
|
var _ Captcha = (*captcha)(nil)
|
|
|
|
var (
|
|
GraphImageErr = errors.New("graph image is invalid")
|
|
GenerateDataErr = errors.New("data generation failed")
|
|
ImageTypeErr = errors.New("tile image must be of type image.Image")
|
|
ShadowImageTypeErr = errors.New("tile shadow image must be of type image.Image")
|
|
MaskImageTypeErr = errors.New("tile mask image must be of type image.Image")
|
|
EmptyBackgroundImageErr = errors.New("no background image")
|
|
)
|
|
|
|
// captcha is the concrete implementation of the Captcha interface
|
|
type captcha struct {
|
|
version string
|
|
logger logger.Logger
|
|
drawImage DrawImage
|
|
opts *Options
|
|
resources *Resources
|
|
mode Mode
|
|
}
|
|
|
|
// newWithMode creates a new slide CAPTCHA instance
|
|
// params:
|
|
// - mode: CAPTCHA mode
|
|
// - opts: Optional initial options
|
|
//
|
|
// return: Captcha interface instance
|
|
func newWithMode(mode Mode, opts ...Option) Captcha {
|
|
capt := &captcha{
|
|
logger: logger.New(),
|
|
drawImage: NewDrawImage(),
|
|
opts: NewOptions(),
|
|
resources: NewResources(),
|
|
mode: mode,
|
|
}
|
|
|
|
defaultOptions()(capt.opts)
|
|
defaultResource()(capt.resources)
|
|
|
|
capt.setOptions(opts...)
|
|
|
|
if mode == ModeBasic {
|
|
capt.opts.rangeDeadZoneDirections = []DeadZoneDirectionType{DeadZoneDirectionTypeLeft}
|
|
capt.opts.enableGraphVerticalRandom = false
|
|
}
|
|
|
|
return capt
|
|
}
|
|
|
|
// setOptions sets the CAPTCHA options
|
|
// params:
|
|
// - opts: Options to set
|
|
func (c *captcha) setOptions(opts ...Option) {
|
|
for _, opt := range opts {
|
|
opt(c.opts)
|
|
}
|
|
}
|
|
|
|
// setResources sets the CAPTCHA resources
|
|
// params:
|
|
// - resources: Resources to set
|
|
func (c *captcha) setResources(resources ...Resource) {
|
|
for _, resource := range resources {
|
|
resource(c.resources)
|
|
}
|
|
}
|
|
|
|
// GetOptions gets the CAPTCHA options
|
|
// return: Pointer to options
|
|
func (c *captcha) GetOptions() *Options {
|
|
return c.opts
|
|
}
|
|
|
|
// Generate generates slide CAPTCHA data
|
|
// returns:
|
|
// - CaptchaData: Generated CAPTCHA data
|
|
// - error: Error information
|
|
func (c *captcha) Generate() (CaptchaData, error) {
|
|
if err := c.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
overlayImage, shadowImage, maskImage := c.genGraph()
|
|
if overlayImage == nil || shadowImage == nil || maskImage == nil {
|
|
return nil, GraphImageErr
|
|
}
|
|
|
|
blocks, tilePoint := c.genGraphBlocks(c.opts.imageSize, c.opts.rangeGraphSize, c.opts.genGraphNumber)
|
|
var block *Block
|
|
if len(blocks) > 1 {
|
|
index := helper.RandIndex(len(blocks))
|
|
if index < 0 {
|
|
index = 0
|
|
}
|
|
block = blocks[index]
|
|
} else {
|
|
block = blocks[0]
|
|
}
|
|
|
|
if block == nil {
|
|
return nil, GenerateDataErr
|
|
}
|
|
|
|
var masterImage, masterBgImage, tileImage image.Image
|
|
var err error
|
|
|
|
masterImage, masterBgImage, err = c.genMasterImage(c.opts.imageSize, shadowImage, blocks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tileImage, err = c.genTileImage(maskImage, masterBgImage, overlayImage, block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.mode == ModeBasic {
|
|
block.TileY = block.Y
|
|
block.DY = block.Y
|
|
} else {
|
|
block.TileY = tilePoint.Y
|
|
block.DY = tilePoint.Y
|
|
}
|
|
block.TileX = tilePoint.X
|
|
block.DX = tilePoint.X
|
|
|
|
return &CaptData{
|
|
block: block,
|
|
masterImage: imagedata.NewJPEGImageData(masterImage),
|
|
tileImage: imagedata.NewPNGImageData(tileImage),
|
|
}, nil
|
|
}
|
|
|
|
// genMasterImage generates the master CAPTCHA image and background image
|
|
// params:
|
|
// - size: Image size
|
|
// - shadowImage: Shadow image
|
|
// - blocks: List of blocks
|
|
//
|
|
// returns:
|
|
// - image.Image: Master image
|
|
// - image.Image: Background image
|
|
// - error: Error information
|
|
func (c *captcha) genMasterImage(size *option.Size, shadowImage image.Image, blocks []*Block) (image.Image, image.Image, error) {
|
|
var drawBlocks = make([]*DrawBlock, 0, len(blocks))
|
|
for i := 0; i < len(blocks); i++ {
|
|
block := blocks[i]
|
|
drawBlocks = append(drawBlocks, &DrawBlock{
|
|
X: block.X,
|
|
Y: block.Y,
|
|
Width: block.Width,
|
|
Height: block.Height,
|
|
Angle: block.Angle,
|
|
Block: block,
|
|
Image: shadowImage,
|
|
})
|
|
}
|
|
|
|
return c.drawImage.DrawWithNRGBA(&DrawImageParams{
|
|
Width: size.Width,
|
|
Height: size.Height,
|
|
Background: randgen.RandImage(c.resources.rangBackgrounds),
|
|
Alpha: c.opts.imageAlpha,
|
|
CaptchaDrawBlocks: drawBlocks,
|
|
})
|
|
}
|
|
|
|
// genTileImage generates a tile image
|
|
// params:
|
|
// - maskImage: Mask image
|
|
// - bgImage: Background image
|
|
// - overlayImage: Overlay image
|
|
// - block: Block data
|
|
//
|
|
// returns:
|
|
// - image.Image: Tile image
|
|
// - error: Error information
|
|
func (c *captcha) genTileImage(maskImage image.Image, bgImage image.Image, overlayImage image.Image, block *Block) (image.Image, error) {
|
|
return c.drawImage.DrawWithTemplate(&DrawTplImageParams{
|
|
Background: bgImage,
|
|
MaskImage: maskImage,
|
|
Alpha: c.opts.imageAlpha,
|
|
Width: block.Width,
|
|
Height: block.Height,
|
|
CaptchaDrawBlock: &DrawBlock{
|
|
X: block.X,
|
|
Y: block.Y,
|
|
Width: block.Width,
|
|
Height: block.Height,
|
|
Angle: block.Angle,
|
|
Block: block,
|
|
Image: overlayImage,
|
|
},
|
|
})
|
|
}
|
|
|
|
// randDeadZoneDirection generates a random dead zone direction
|
|
// return: Dead zone direction
|
|
func (c *captcha) randDeadZoneDirection() DeadZoneDirectionType {
|
|
dirs := c.opts.rangeDeadZoneDirections
|
|
|
|
index := helper.RandIndex(len(dirs))
|
|
if index < 0 {
|
|
return 0
|
|
}
|
|
|
|
res := dirs[index]
|
|
return res
|
|
}
|
|
|
|
// randGraphAngle generates a random graph angle
|
|
// return: Random angle value
|
|
func (c *captcha) randGraphAngle() int {
|
|
angles := c.opts.rangeGraphAnglePos
|
|
|
|
index := helper.RandIndex(len(angles))
|
|
if index < 0 {
|
|
return 0
|
|
}
|
|
|
|
angle := angles[index]
|
|
res := random.RandInt(angle.Min, angle.Max)
|
|
|
|
return res
|
|
}
|
|
|
|
// genGraphBlocks generates graph block data
|
|
// params:
|
|
// - imageSize: Main image size
|
|
// - size: Graph size range
|
|
// - length: Number of graphs
|
|
//
|
|
// returns:
|
|
// - []*Block: List of blocks
|
|
// - *option.Point: Tile position
|
|
func (c *captcha) genGraphBlocks(imageSize *option.Size, size *option.RangeVal, length int) ([]*Block, *option.Point) {
|
|
var blocks = make([]*Block, 0, length)
|
|
width := imageSize.Width
|
|
height := imageSize.Height
|
|
|
|
randAngle := c.randGraphAngle()
|
|
randSize := random.RandInt(size.Min, size.Max)
|
|
cHeight := randSize
|
|
cWidth := randSize
|
|
|
|
dzdType := c.randDeadZoneDirection()
|
|
dp := cWidth / 2
|
|
blockWidth := (width - cWidth - 20) / length
|
|
y := c.calcYWithDeadZone(5, height-cHeight-5, cHeight, dzdType)
|
|
|
|
for i := 0; i < length; i++ {
|
|
var block = &Block{}
|
|
start, end := c.calcXWithDeadZone((i*blockWidth)+dp+5, ((i+1)*blockWidth)-dp, cWidth, dzdType)
|
|
|
|
start = int(math.Max(float64(start), float64(dp+5)))
|
|
block.X = random.RandInt(start+20, end+20) - dp
|
|
|
|
if c.opts.enableGraphVerticalRandom {
|
|
y = c.calcYWithDeadZone(5, height-cHeight-5, cHeight, dzdType)
|
|
}
|
|
|
|
block.Y = y
|
|
block.Width = cWidth
|
|
block.Height = cHeight
|
|
block.Angle = randAngle
|
|
|
|
blocks = append(blocks, block)
|
|
}
|
|
|
|
point := &option.Point{}
|
|
if c.mode == ModeBasic {
|
|
point.X = random.RandInt(5, dp)
|
|
point.Y = y
|
|
return blocks, point
|
|
}
|
|
|
|
if dzdType == DeadZoneDirectionTypeTop {
|
|
point.X = random.RandInt(5, width-cWidth-5)
|
|
point.Y = 5
|
|
} else if dzdType == DeadZoneDirectionTypeBottom {
|
|
point.X = random.RandInt(5, width-cWidth-5)
|
|
point.Y = height - cHeight - 5
|
|
} else if dzdType == DeadZoneDirectionTypeLeft {
|
|
point.X = 5
|
|
point.Y = random.RandInt(5, height-cHeight-5)
|
|
} else if dzdType == DeadZoneDirectionTypeRight {
|
|
point.X = width - cWidth - 5
|
|
point.Y = random.RandInt(5, height-cHeight-5)
|
|
}
|
|
|
|
return blocks, point
|
|
}
|
|
|
|
// calcXWithDeadZone calculates the X coordinate range (considering dead zone)
|
|
// params:
|
|
// - start: Start X coordinate
|
|
// - end: End X coordinate
|
|
// - value: Block width
|
|
// - dzdType: Dead zone direction
|
|
//
|
|
// returns:
|
|
// - int: Adjusted start X coordinate
|
|
// - int: Adjusted end X coordinate
|
|
func (c *captcha) calcXWithDeadZone(start, end, value int, dzdType DeadZoneDirectionType) (int, int) {
|
|
if dzdType == DeadZoneDirectionTypeLeft {
|
|
start += value
|
|
end += value
|
|
}
|
|
return start, end
|
|
}
|
|
|
|
// calcYWithDeadZone calculates the Y coordinate (considering dead zone)
|
|
// params:
|
|
// - start: Start Y coordinate
|
|
// - end: End Y coordinate
|
|
// - value: Block height
|
|
// - dzdType: Dead zone direction
|
|
//
|
|
// return: Random Y coordinate
|
|
func (c *captcha) calcYWithDeadZone(start, end, value int, dzdType DeadZoneDirectionType) int {
|
|
if dzdType == DeadZoneDirectionTypeTop {
|
|
start += value
|
|
} else if dzdType == DeadZoneDirectionTypeBottom {
|
|
end -= value
|
|
}
|
|
return random.RandInt(start, end)
|
|
}
|
|
|
|
// genGraph generates random graph resources
|
|
// returns:
|
|
// - maskImage: Mask image
|
|
// - shadowImage: Shadow image
|
|
// - templateImage: Template image
|
|
func (c *captcha) genGraph() (maskImage, shadowImage, templateImage image.Image) {
|
|
index := helper.RandIndex(len(c.resources.rangGraphImage))
|
|
if index < 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
graphImage := c.resources.rangGraphImage[index]
|
|
|
|
return graphImage.OverlayImage, graphImage.ShadowImage, graphImage.MaskImage
|
|
}
|
|
|
|
// check checks the CAPTCHA parameters
|
|
// return: Error information
|
|
func (c *captcha) check() error {
|
|
for _, tile := range c.resources.rangGraphImage {
|
|
if tile.OverlayImage == nil {
|
|
return ImageTypeErr
|
|
} else if tile.ShadowImage == nil {
|
|
return ShadowImageTypeErr
|
|
} else if tile.MaskImage == nil {
|
|
return MaskImageTypeErr
|
|
}
|
|
}
|
|
|
|
if len(c.resources.rangBackgrounds) == 0 {
|
|
return EmptyBackgroundImageErr
|
|
}
|
|
|
|
return nil
|
|
}
|