Files
go-captcha/v2/slide/slide.go
2025-05-19 00:53:58 +08:00

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
}