package auth import ( "bytes" "fmt" "image" "image/color" "image/draw" "image/png" "math/rand" "time" ) // 验证码图像渲染:内置 5x7 点阵数字 + 噪点 + 干扰线,不依赖第三方字体库 // (PLAN §4:内置生成,不依赖第三方服务)。 // digitGlyphs 为 0-9 的 5x7 点阵,每行低 5 位表示该行像素。 var digitGlyphs = [10][7]byte{ {0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110}, // 0 {0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110}, // 1 {0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111}, // 2 {0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110}, // 3 {0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010}, // 4 {0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110}, // 5 {0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110}, // 6 {0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000}, // 7 {0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110}, // 8 {0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100}, // 9 } const ( glyphW = 5 glyphH = 7 scale = 3 // 点阵放大倍数 charGap = 4 // 字符间距(像素) edgePad = 6 // 画布边距 ) // RenderCaptchaPNG 将 4 位数字验证码渲染为 PNG 字节流。 // 仅接受数字字符,其余返回错误。 func RenderCaptchaPNG(text string) ([]byte, error) { if len(text) == 0 || len(text) > 8 { return nil, fmt.Errorf("auth: captcha text length must be 1~8") } for _, r := range text { if r < '0' || r > '9' { return nil, fmt.Errorf("auth: captcha text must be digits") } } canvasW := edgePad*2 + len(text)*glyphW*scale + (len(text)-1)*charGap canvasH := edgePad*2 + glyphH*scale img := image.NewRGBA(image.Rect(0, 0, canvasW, canvasH)) draw.Draw(img, img.Bounds(), &image.Uniform{C: color.RGBA{248, 250, 252, 255}}, image.Point{}, draw.Src) rng := rand.New(rand.NewSource(time.Now().UnixNano())) // 噪点(浅灰,稀疏) for i := 0; i < canvasW*canvasH/7; i++ { x, y := rng.Intn(canvasW), rng.Intn(canvasH) g := uint8(150 + rng.Intn(90)) img.Set(x, y, color.RGBA{g, g, g, 255}) } // 干扰线(穿过字符区域的浅色斜线) for i := 0; i < 3; i++ { g := uint8(160 + rng.Intn(80)) c := color.RGBA{g, g, g, 255} x1, y1 := rng.Intn(canvasW/2), rng.Intn(canvasH) x2, y2 := canvasW/2+rng.Intn(canvasW/2), rng.Intn(canvasH) drawLine(img, x1, y1, x2, y2, c) } // 逐字符绘制(颜色随机取深色系) inkPalette := []color.RGBA{ {40, 60, 110, 255}, {120, 45, 45, 255}, {30, 90, 60, 255}, {80, 60, 110, 255}, } for i, r := range text { glyph := digitGlyphs[r-'0'] ink := inkPalette[rng.Intn(len(inkPalette))] x0 := edgePad + i*(glyphW*scale+charGap) y0 := edgePad for row := 0; row < glyphH; row++ { for col := 0; col < glyphW; col++ { if glyph[row]&(1<<(glyphW-1-col)) != 0 { fillRect(img, x0+col*scale, y0+row*scale, scale, scale, ink) } } } } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { return nil, fmt.Errorf("auth: encode captcha png: %w", err) } return buf.Bytes(), nil } // fillRect 填充实心矩形。 func fillRect(img *image.RGBA, x, y, w, h int, c color.RGBA) { for dy := 0; dy < h; dy++ { for dx := 0; dx < w; dx++ { img.Set(x+dx, y+dy, c) } } } // drawLine 使用 DDA 算法画线。 func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.RGBA) { steps := abs(x2-x1) if d := abs(y2 - y1); d > steps { steps = d } if steps == 0 { img.Set(x1, y1, c) return } for i := 0; i <= steps; i++ { x := x1 + (x2-x1)*i/steps y := y1 + (y2-y1)*i/steps if x >= 0 && x < img.Bounds().Dx() && y >= 0 && y < img.Bounds().Dy() { img.Set(x, y, c) } } } func abs(n int) int { if n < 0 { return -n } return n }