复制项目
This commit is contained in:
292
internal/api/util/api.go
Normal file
292
internal/api/util/api.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
type Api struct {
|
||||
ImUserID string
|
||||
ProxyHeader string
|
||||
ChatAdminUserID string
|
||||
}
|
||||
|
||||
func (o *Api) WithAdminUser(ctx context.Context) context.Context {
|
||||
return mctx.WithAdminUser(ctx, o.ChatAdminUserID)
|
||||
}
|
||||
|
||||
// isPrivateIP 检查IP是否是内网IP
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
// 检查是否是内网IP范围
|
||||
// 10.0.0.0/8
|
||||
// 172.16.0.0/12
|
||||
// 192.168.0.0/16
|
||||
// 127.0.0.0/8 (localhost)
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return false
|
||||
}
|
||||
// 10.0.0.0/8
|
||||
if ip4[0] == 10 {
|
||||
return true
|
||||
}
|
||||
// 172.16.0.0/12
|
||||
if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
|
||||
return true
|
||||
}
|
||||
// 192.168.0.0/16
|
||||
if ip4[0] == 192 && ip4[1] == 168 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseForwardedHeader 解析标准的 Forwarded 头(RFC 7239)
|
||||
// 格式:Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
func parseForwardedHeader(forwarded string) string {
|
||||
// 查找 for= 后面的IP地址
|
||||
parts := strings.Split(forwarded, ";")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(strings.ToLower(part), "for=") {
|
||||
ip := strings.TrimPrefix(part, "for=")
|
||||
ip = strings.TrimPrefix(ip, "For=")
|
||||
ip = strings.TrimPrefix(ip, "FOR=")
|
||||
// 移除可能的引号和端口号
|
||||
ip = strings.Trim(ip, `"`)
|
||||
if idx := strings.Index(ip, ":"); idx != -1 {
|
||||
ip = ip[:idx]
|
||||
}
|
||||
if parsedIP := net.ParseIP(ip); parsedIP != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetClientIP 获取客户端真实IP地址
|
||||
// 支持多层反向代理和CDN场景
|
||||
// 优先级:
|
||||
// 1. CDN特定头(CF-Connecting-IP, True-Client-IP等)- 最可靠
|
||||
// 2. 标准 Forwarded 头(RFC 7239)- 现代标准
|
||||
// 3. X-Real-IP - 通常由第一层代理设置,比X-Forwarded-For更可靠
|
||||
// 4. X-Forwarded-For - 取最左边的IP(客户端真实IP),格式:client_ip, proxy1_ip, proxy2_ip, ...
|
||||
// 5. 其他常见头(X-Client-IP, True-Client-IP等)
|
||||
// 6. 自定义ProxyHeader
|
||||
// 7. RemoteAddr - 最后回退
|
||||
func (o *Api) GetClientIP(c *gin.Context) (string, error) {
|
||||
// 记录所有相关的HTTP头用于调试
|
||||
headers := map[string]string{
|
||||
"Forwarded": c.Request.Header.Get("Forwarded"), // RFC 7239 标准头
|
||||
"X-Forwarded-For": c.Request.Header.Get("X-Forwarded-For"),
|
||||
"X-Real-IP": c.Request.Header.Get("X-Real-IP"),
|
||||
"CF-Connecting-IP": c.Request.Header.Get("CF-Connecting-IP"), // Cloudflare - 真实客户端IP
|
||||
"CF-Ray": c.Request.Header.Get("CF-Ray"), // Cloudflare - 请求ID,用于验证来自CF
|
||||
"True-Client-IP": c.Request.Header.Get("True-Client-IP"), // Cloudflare Enterprise, Akamai
|
||||
"X-Client-IP": c.Request.Header.Get("X-Client-IP"), // 一些代理
|
||||
"X-Forwarded": c.Request.Header.Get("X-Forwarded"), // 一些代理
|
||||
"Forwarded-For": c.Request.Header.Get("Forwarded-For"), // 非标准,但有些代理使用
|
||||
"X-Cluster-Client-IP": c.Request.Header.Get("X-Cluster-Client-IP"), // Kubernetes
|
||||
"RemoteAddr": c.Request.RemoteAddr,
|
||||
}
|
||||
if o.ProxyHeader != "" {
|
||||
headers["Custom-"+o.ProxyHeader] = c.Request.Header.Get(o.ProxyHeader)
|
||||
}
|
||||
|
||||
// 打印所有能获取到的IP相关信息用于调试
|
||||
customProxyHeader := ""
|
||||
if o.ProxyHeader != "" {
|
||||
customProxyHeader = headers["Custom-"+o.ProxyHeader]
|
||||
}
|
||||
log.ZInfo(c, "GetClientIP 调试信息 - 所有HTTP头",
|
||||
"Forwarded", headers["Forwarded"],
|
||||
"X-Forwarded-For", headers["X-Forwarded-For"],
|
||||
"X-Real-IP", headers["X-Real-IP"],
|
||||
"CF-Connecting-IP", headers["CF-Connecting-IP"],
|
||||
"CF-Ray", headers["CF-Ray"],
|
||||
"True-Client-IP", headers["True-Client-IP"],
|
||||
"X-Client-IP", headers["X-Client-IP"],
|
||||
"X-Forwarded", headers["X-Forwarded"],
|
||||
"Forwarded-For", headers["Forwarded-For"],
|
||||
"X-Cluster-Client-IP", headers["X-Cluster-Client-IP"],
|
||||
"RemoteAddr", headers["RemoteAddr"],
|
||||
"Custom-ProxyHeader", customProxyHeader)
|
||||
|
||||
// 1. 优先检查 Cloudflare 的 CF-Connecting-IP(最可靠,Cloudflare 直接设置真实客户端IP)
|
||||
// 检查是否来自 Cloudflare(通过 CF-Ray 头判断)
|
||||
cfRay := c.Request.Header.Get("CF-Ray")
|
||||
cfConnectingIP := c.Request.Header.Get("CF-Connecting-IP")
|
||||
if cfConnectingIP != "" {
|
||||
// 特别记录 Cloudflare 相关信息
|
||||
log.ZInfo(c, "GetClientIP detected Cloudflare request",
|
||||
"CF-Connecting-IP", cfConnectingIP,
|
||||
"CF-Ray", cfRay)
|
||||
|
||||
ip := strings.TrimSpace(cfConnectingIP)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
// Cloudflare 的 CF-Connecting-IP 总是包含真实客户端IP,无论公网还是内网
|
||||
// 如果同时有 CF-Ray,说明确实来自 Cloudflare,更加可靠
|
||||
if cfRay != "" {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "CF-Connecting-IP",
|
||||
"cfRay", cfRay,
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
} else {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "CF-Connecting-IP",
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查其他CDN特定的头
|
||||
cdnHeaders := []string{
|
||||
"True-Client-IP", // Cloudflare Enterprise, Akamai
|
||||
"X-Client-IP", // 一些CDN和代理
|
||||
"X-Cluster-Client-IP", // Kubernetes Ingress
|
||||
}
|
||||
for _, headerName := range cdnHeaders {
|
||||
ipStr := c.Request.Header.Get(headerName)
|
||||
if ipStr != "" {
|
||||
ip := strings.TrimSpace(ipStr)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
// CDN头通常包含真实客户端IP,优先返回公网IP
|
||||
if !isPrivateIP(parsedIP) {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", headerName,
|
||||
"isPrivateIP", false)
|
||||
return ip, nil
|
||||
}
|
||||
// 即使是内网IP,CDN头也相对可靠
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", headerName,
|
||||
"isPrivateIP", true)
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查标准 Forwarded 头(RFC 7239)- 现代标准,最可靠
|
||||
forwarded := c.Request.Header.Get("Forwarded")
|
||||
if forwarded != "" {
|
||||
ip := parseForwardedHeader(forwarded)
|
||||
if ip != "" {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "Forwarded",
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查 X-Real-IP 头(通常由第一层代理设置,比X-Forwarded-For更可靠)
|
||||
xRealIP := c.Request.Header.Get("X-Real-IP")
|
||||
if xRealIP != "" {
|
||||
ip := strings.TrimSpace(xRealIP)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "X-Real-IP",
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 检查 X-Forwarded-For 头(格式:client_ip, proxy1_ip, proxy2_ip, ...)
|
||||
// 重要:在多层代理中,最左边的IP是客户端真实IP,应该优先取第一个IP
|
||||
xForwardedFor := c.Request.Header.Get("X-Forwarded-For")
|
||||
if xForwardedFor != "" {
|
||||
ips := strings.Split(xForwardedFor, ",")
|
||||
// 取最左边的IP(第一个IP),这是客户端真实IP
|
||||
// 格式:client_ip, proxy1_ip, proxy2_ip, ...
|
||||
if len(ips) > 0 {
|
||||
ip := strings.TrimSpace(ips[0])
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "X-Forwarded-For",
|
||||
"totalIPs", len(ips),
|
||||
"isPrivateIP", isPrivateIP(parsedIP),
|
||||
"note", "取最左边的IP(客户端真实IP)")
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 检查其他常见头
|
||||
otherHeaders := []string{"X-Client-IP", "X-Forwarded", "Forwarded-For"}
|
||||
for _, headerName := range otherHeaders {
|
||||
ipStr := c.Request.Header.Get(headerName)
|
||||
if ipStr != "" {
|
||||
ip := strings.TrimSpace(ipStr)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", headerName,
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 如果配置了自定义的 ProxyHeader,检查它
|
||||
if o.ProxyHeader != "" {
|
||||
customHeaderIP := c.Request.Header.Get(o.ProxyHeader)
|
||||
if customHeaderIP != "" {
|
||||
ip := strings.TrimSpace(customHeaderIP)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "Custom-"+o.ProxyHeader,
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 最后使用 RemoteAddr(在集群环境中可能是代理服务器的IP)
|
||||
remoteAddr := c.Request.RemoteAddr
|
||||
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
log.ZError(c, "GetClientIP failed to parse RemoteAddr", err, "RemoteAddr", remoteAddr)
|
||||
return "", errs.ErrInternalServer.WrapMsg(fmt.Sprintf("failed to parse RemoteAddr: %v", err))
|
||||
}
|
||||
parsedIP := net.ParseIP(ip)
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "RemoteAddr",
|
||||
"isPrivateIP", parsedIP != nil && isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (o *Api) GetDefaultIMAdminUserID() string {
|
||||
return o.ImUserID
|
||||
}
|
||||
783
internal/api/util/captcha.go
Normal file
783
internal/api/util/captcha.go
Normal file
@@ -0,0 +1,783 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
// GenerateCaptchaImageFromCode 根据给定的验证码生成动态GIF图片
|
||||
// 返回GIF图片的字节数组
|
||||
func GenerateCaptchaImageFromCode(code string) (imgBytes []byte, err error) {
|
||||
// 图片尺寸 - 宽度280px,高度50px
|
||||
width := 280
|
||||
height := 50
|
||||
|
||||
// GIF动画参数
|
||||
frameCount := 12 // 12帧动画,更流畅
|
||||
delay := 8 // 每帧延迟8(单位:1/100秒)
|
||||
|
||||
// 创建统一的调色板(所有帧共享)
|
||||
sharedPalette := createSharedPalette()
|
||||
|
||||
// 创建GIF
|
||||
g := &gif.GIF{
|
||||
Image: []*image.Paletted{},
|
||||
Delay: []int{},
|
||||
}
|
||||
|
||||
// 使用固定种子保证数字位置基本一致
|
||||
baseSeed := time.Now().UnixNano()
|
||||
|
||||
// 预生成每帧的基础数据(干扰线、圆圈等的位置)
|
||||
noiseData := generateNoiseData(baseSeed, frameCount, width, height)
|
||||
|
||||
// 生成每一帧
|
||||
for i := 0; i < frameCount; i++ {
|
||||
frame := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// 设置背景色(浅灰色,稍微模糊)
|
||||
bgColor := color.RGBA{235, 235, 235, 255}
|
||||
draw.Draw(frame, frame.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
|
||||
|
||||
// 使用随机数生成器(每帧略有不同)
|
||||
rng := rand.New(rand.NewSource(baseSeed + int64(i*1000)))
|
||||
|
||||
// 绘制丰富的背景 - 添加渐变背景
|
||||
drawGradientBackground(frame, rng, width, height)
|
||||
|
||||
// 绘制移动的背景圆圈(类似bubbles效果)
|
||||
drawAnimatedCircles(frame, noiseData, i, width, height)
|
||||
|
||||
// 绘制额外的背景形状(矩形、椭圆等)
|
||||
drawAdditionalShapes(frame, rng, width, height)
|
||||
|
||||
// 添加动态干扰线(位置会变化)
|
||||
drawAnimatedNoiseLines(frame, noiseData, i, width, height)
|
||||
|
||||
// 决定是否显示数字(每隔几帧就隐藏数字,造成闪烁效果)
|
||||
// 例如:0,1,2显示数字,3,4隐藏,5,6显示,7,8隐藏,9,10显示,11隐藏
|
||||
showNumbers := (i % 3) != 2 // 每3帧中有2帧显示数字,1帧隐藏
|
||||
|
||||
if showNumbers {
|
||||
// 绘制验证码文字(位置会抖动)
|
||||
drawNumbersWithWiggle(frame, code, rng, width, height, i, frameCount)
|
||||
}
|
||||
|
||||
// 添加动态干扰点(位置会变化)
|
||||
drawAnimatedNoiseDots(frame, noiseData, i, width, height)
|
||||
|
||||
// 添加模糊噪点 - 增加噪点密度
|
||||
addBlurNoise(frame, rng)
|
||||
|
||||
// 添加额外的随机干扰线
|
||||
drawRandomNoiseLines(frame, rng, width, height)
|
||||
|
||||
// 转换为调色板图像(GIF需要)
|
||||
// 使用统一的调色板
|
||||
paletted := convertToPalettedWithPalette(frame, sharedPalette)
|
||||
|
||||
if paletted == nil {
|
||||
return nil, errs.New(fmt.Sprintf("failed to convert frame %d to paletted", i+1))
|
||||
}
|
||||
|
||||
g.Image = append(g.Image, paletted)
|
||||
g.Delay = append(g.Delay, delay)
|
||||
}
|
||||
|
||||
// 编码为GIF
|
||||
var buf bytes.Buffer
|
||||
err = gif.EncodeAll(&buf, g)
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "encode gif failed")
|
||||
}
|
||||
|
||||
// 验证数据不为空
|
||||
if buf.Len() == 0 {
|
||||
return nil, errs.New("generated gif data is empty")
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// NoiseData 存储每帧的干扰数据
|
||||
type NoiseData struct {
|
||||
lines [][]LineData
|
||||
circles [][]CircleData
|
||||
dots [][]DotData
|
||||
}
|
||||
|
||||
type LineData struct {
|
||||
x1, y1, x2, y2 int
|
||||
color color.RGBA
|
||||
thickness int
|
||||
}
|
||||
|
||||
type CircleData struct {
|
||||
x, y, radius int
|
||||
color color.RGBA
|
||||
}
|
||||
|
||||
type DotData struct {
|
||||
x, y int
|
||||
color color.RGBA
|
||||
size int
|
||||
}
|
||||
|
||||
// generateNoiseData 预生成所有帧的干扰数据
|
||||
func generateNoiseData(baseSeed int64, frameCount, width, height int) *NoiseData {
|
||||
rng := rand.New(rand.NewSource(baseSeed))
|
||||
|
||||
data := &NoiseData{
|
||||
lines: make([][]LineData, frameCount),
|
||||
circles: make([][]CircleData, frameCount),
|
||||
dots: make([][]DotData, frameCount),
|
||||
}
|
||||
|
||||
// 生成干扰线数据(每帧位置略有变化)- 增加干扰线数量
|
||||
lineCount := 20 // 从10增加到20
|
||||
for frame := 0; frame < frameCount; frame++ {
|
||||
lines := make([]LineData, lineCount)
|
||||
for i := 0; i < lineCount; i++ {
|
||||
// 基础位置
|
||||
baseX1 := rng.Intn(width)
|
||||
baseY1 := rng.Intn(height)
|
||||
baseX2 := rng.Intn(width)
|
||||
baseY2 := rng.Intn(height)
|
||||
|
||||
// 每帧添加偏移,形成移动效果
|
||||
offsetX := int(float64(frame) * 2.5 * float64(rng.Float64()-0.5))
|
||||
offsetY := int(float64(frame) * 2.5 * float64(rng.Float64()-0.5))
|
||||
|
||||
lines[i] = LineData{
|
||||
x1: clampCoord(baseX1+offsetX, width),
|
||||
y1: clampCoord(baseY1+offsetY, height),
|
||||
x2: clampCoord(baseX2+offsetX, width),
|
||||
y2: clampCoord(baseY2+offsetY, height),
|
||||
color: color.RGBA{
|
||||
uint8(150 + rng.Intn(80)), // 更宽的颜色范围
|
||||
uint8(150 + rng.Intn(80)),
|
||||
uint8(150 + rng.Intn(80)),
|
||||
200 + uint8(rng.Intn(55)), // 半透明
|
||||
},
|
||||
thickness: 1 + rng.Intn(3), // 增加线条粗细变化
|
||||
}
|
||||
}
|
||||
data.lines[frame] = lines
|
||||
}
|
||||
|
||||
// 生成背景圆圈数据(移动的bubbles)- 增加圆圈数量和大小变化
|
||||
circleCount := 15 // 从8增加到15
|
||||
for frame := 0; frame < frameCount; frame++ {
|
||||
circles := make([]CircleData, circleCount)
|
||||
for i := 0; i < circleCount; i++ {
|
||||
// 基础位置
|
||||
baseX := rng.Intn(width*2) - width/2
|
||||
baseY := rng.Intn(height*2) - height/2
|
||||
|
||||
// 每帧移动
|
||||
moveX := int(float64(frame) * 1.5 * float64(rng.Float64()-0.5))
|
||||
moveY := int(float64(frame) * 1.5 * float64(rng.Float64()-0.5))
|
||||
|
||||
circles[i] = CircleData{
|
||||
x: clampCoord(baseX+moveX, width),
|
||||
y: clampCoord(baseY+moveY, height),
|
||||
radius: 8 + rng.Intn(35), // 更大的半径范围(8-43)
|
||||
color: color.RGBA{
|
||||
uint8(180 + rng.Intn(50)), // 更宽的颜色范围
|
||||
uint8(180 + rng.Intn(50)),
|
||||
uint8(180 + rng.Intn(50)),
|
||||
150 + uint8(rng.Intn(105)), // 更宽的透明度范围
|
||||
},
|
||||
}
|
||||
}
|
||||
data.circles[frame] = circles
|
||||
}
|
||||
|
||||
// 生成干扰点数据(闪烁效果)- 增加点数量
|
||||
dotCount := 200 // 从100增加到200
|
||||
for frame := 0; frame < frameCount; frame++ {
|
||||
dots := make([]DotData, dotCount)
|
||||
for i := 0; i < dotCount; i++ {
|
||||
x := rng.Intn(width)
|
||||
y := rng.Intn(height)
|
||||
|
||||
// 某些点会在某些帧消失(闪烁效果)
|
||||
visible := frame%3 != i%3 || rng.Float64() > 0.3
|
||||
|
||||
dots[i] = DotData{
|
||||
x: x,
|
||||
y: y,
|
||||
color: color.RGBA{
|
||||
uint8(120 + rng.Intn(110)), // 更宽的颜色范围
|
||||
uint8(120 + rng.Intn(110)),
|
||||
uint8(120 + rng.Intn(110)),
|
||||
func() uint8 {
|
||||
if visible {
|
||||
return 200 + uint8(rng.Intn(55))
|
||||
}
|
||||
return uint8(80 + rng.Intn(120))
|
||||
}(),
|
||||
},
|
||||
size: 1 + rng.Intn(3), // 更大的点尺寸(1-3)
|
||||
}
|
||||
}
|
||||
data.dots[frame] = dots
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func clampCoord(v, max int) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v >= max {
|
||||
return max - 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// createSharedPalette 创建统一的调色板(所有帧共享)
|
||||
func createSharedPalette() color.Palette {
|
||||
palette := make(color.Palette, 256)
|
||||
|
||||
// 添加灰色渐变(0-239)
|
||||
for i := 0; i < 240; i++ {
|
||||
val := uint8(i * 255 / 240)
|
||||
palette[i] = color.RGBA{val, val, val, 255}
|
||||
}
|
||||
|
||||
// 添加常用颜色(240-255)
|
||||
colorMap := map[int]color.RGBA{
|
||||
240: {30, 30, 30, 255},
|
||||
241: {60, 60, 60, 255},
|
||||
242: {50, 50, 50, 255},
|
||||
243: {40, 40, 80, 255},
|
||||
244: {80, 40, 40, 255},
|
||||
245: {50, 80, 50, 255},
|
||||
246: {100, 30, 30, 255},
|
||||
247: {30, 100, 30, 255},
|
||||
248: {30, 30, 100, 255},
|
||||
249: {140, 140, 140, 255},
|
||||
250: {160, 160, 160, 255},
|
||||
251: {180, 180, 180, 255},
|
||||
252: {200, 200, 200, 255},
|
||||
253: {220, 220, 220, 255},
|
||||
254: {235, 235, 235, 255},
|
||||
255: {255, 255, 255, 255},
|
||||
}
|
||||
|
||||
for idx, col := range colorMap {
|
||||
if idx < 256 {
|
||||
palette[idx] = col
|
||||
}
|
||||
}
|
||||
|
||||
return palette
|
||||
}
|
||||
|
||||
// convertToPalettedWithPalette 使用指定调色板将RGBA图像转换为Paletted图像
|
||||
func convertToPalettedWithPalette(img *image.RGBA, palette color.Palette) *image.Paletted {
|
||||
bounds := img.Bounds()
|
||||
|
||||
// 创建Paletted图像
|
||||
paletted := image.NewPaletted(bounds, palette)
|
||||
|
||||
// 转换:找到最接近的调色板颜色
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := img.At(x, y)
|
||||
// 找到调色板中最接近的颜色
|
||||
idx := findClosestColor(c, palette)
|
||||
paletted.SetColorIndex(x, y, uint8(idx))
|
||||
}
|
||||
}
|
||||
|
||||
return paletted
|
||||
}
|
||||
|
||||
// findClosestColor 在调色板中找到最接近的颜色索引
|
||||
func findClosestColor(c color.Color, palette color.Palette) int {
|
||||
r, g, b, _ := c.RGBA()
|
||||
minDist := uint32(^uint32(0))
|
||||
bestIdx := 0
|
||||
|
||||
for i := 0; i < len(palette); i++ {
|
||||
cr, cg, cb, _ := palette[i].RGBA()
|
||||
// 计算欧氏距离
|
||||
dr := (r >> 8) - (cr >> 8)
|
||||
dg := (g >> 8) - (cg >> 8)
|
||||
db := (b >> 8) - (cb >> 8)
|
||||
dist := dr*dr + dg*dg + db*db
|
||||
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
bestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
return bestIdx
|
||||
}
|
||||
|
||||
// drawGradientBackground 绘制渐变背景
|
||||
func drawGradientBackground(img *image.RGBA, rng *rand.Rand, width, height int) {
|
||||
// 随机选择渐变方向
|
||||
vertical := rng.Intn(2) == 0
|
||||
|
||||
if vertical {
|
||||
// 垂直渐变
|
||||
startR := uint8(220 + rng.Intn(35))
|
||||
startG := uint8(220 + rng.Intn(35))
|
||||
startB := uint8(220 + rng.Intn(35))
|
||||
endR := uint8(200 + rng.Intn(55))
|
||||
endG := uint8(200 + rng.Intn(55))
|
||||
endB := uint8(200 + rng.Intn(55))
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
ratio := float64(y) / float64(height)
|
||||
r := uint8(float64(startR)*(1-ratio) + float64(endR)*ratio)
|
||||
g := uint8(float64(startG)*(1-ratio) + float64(endG)*ratio)
|
||||
b := uint8(float64(startB)*(1-ratio) + float64(endB)*ratio)
|
||||
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, color.RGBA{r, g, b, 255})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 水平渐变
|
||||
startR := uint8(220 + rng.Intn(35))
|
||||
startG := uint8(220 + rng.Intn(35))
|
||||
startB := uint8(220 + rng.Intn(35))
|
||||
endR := uint8(200 + rng.Intn(55))
|
||||
endG := uint8(200 + rng.Intn(55))
|
||||
endB := uint8(200 + rng.Intn(55))
|
||||
|
||||
for x := 0; x < width; x++ {
|
||||
ratio := float64(x) / float64(width)
|
||||
r := uint8(float64(startR)*(1-ratio) + float64(endR)*ratio)
|
||||
g := uint8(float64(startG)*(1-ratio) + float64(endG)*ratio)
|
||||
b := uint8(float64(startB)*(1-ratio) + float64(endB)*ratio)
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
img.Set(x, y, color.RGBA{r, g, b, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawAdditionalShapes 绘制额外的背景形状
|
||||
func drawAdditionalShapes(img *image.RGBA, rng *rand.Rand, width, height int) {
|
||||
shapeCount := 5 + rng.Intn(8) // 5-12个形状
|
||||
|
||||
for i := 0; i < shapeCount; i++ {
|
||||
shapeType := rng.Intn(3)
|
||||
x := rng.Intn(width)
|
||||
y := rng.Intn(height)
|
||||
|
||||
// 随机颜色(浅色,半透明)
|
||||
col := color.RGBA{
|
||||
uint8(190 + rng.Intn(45)),
|
||||
uint8(190 + rng.Intn(45)),
|
||||
uint8(190 + rng.Intn(45)),
|
||||
uint8(100 + rng.Intn(100)),
|
||||
}
|
||||
|
||||
switch shapeType {
|
||||
case 0: // 矩形
|
||||
w := 15 + rng.Intn(40)
|
||||
h := 15 + rng.Intn(40)
|
||||
drawRect(img, x, y, w, h, col)
|
||||
case 1: // 椭圆
|
||||
rx := 10 + rng.Intn(25)
|
||||
ry := 10 + rng.Intn(25)
|
||||
drawEllipse(img, x, y, rx, ry, col)
|
||||
case 2: // 小圆圈
|
||||
radius := 5 + rng.Intn(15)
|
||||
drawCircle(img, x, y, radius, col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawRect 绘制矩形
|
||||
func drawRect(img *image.RGBA, x, y, w, h int, c color.Color) {
|
||||
for dy := 0; dy < h && y+dy < img.Bounds().Dy(); dy++ {
|
||||
if y+dy < 0 {
|
||||
continue
|
||||
}
|
||||
for dx := 0; dx < w && x+dx < img.Bounds().Dx(); dx++ {
|
||||
if x+dx < 0 {
|
||||
continue
|
||||
}
|
||||
r, g, b, _ := img.At(x+dx, y+dy).RGBA()
|
||||
cr, cg, cb, ca := c.RGBA()
|
||||
|
||||
alpha := uint32(ca >> 8)
|
||||
invAlpha := 255 - alpha
|
||||
|
||||
newR := uint8(((uint32(r>>8) * invAlpha) + (uint32(cr>>8) * alpha)) / 255)
|
||||
newG := uint8(((uint32(g>>8) * invAlpha) + (uint32(cg>>8) * alpha)) / 255)
|
||||
newB := uint8(((uint32(b>>8) * invAlpha) + (uint32(cb>>8) * alpha)) / 255)
|
||||
|
||||
img.Set(x+dx, y+dy, color.RGBA{newR, newG, newB, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawEllipse 绘制椭圆
|
||||
func drawEllipse(img *image.RGBA, cx, cy, rx, ry int, c color.Color) {
|
||||
for y := -ry; y <= ry; y++ {
|
||||
for x := -rx; x <= rx; x++ {
|
||||
// 椭圆方程: (x/rx)^2 + (y/ry)^2 <= 1
|
||||
if float64(x*x)/(float64(rx*rx))+float64(y*y)/(float64(ry*ry)) <= 1.0 {
|
||||
px := cx + x
|
||||
py := cy + y
|
||||
if px >= 0 && px < img.Bounds().Dx() && py >= 0 && py < img.Bounds().Dy() {
|
||||
r, g, b, _ := img.At(px, py).RGBA()
|
||||
cr, cg, cb, ca := c.RGBA()
|
||||
|
||||
alpha := uint32(ca >> 8)
|
||||
invAlpha := 255 - alpha
|
||||
|
||||
newR := uint8(((uint32(r>>8) * invAlpha) + (uint32(cr>>8) * alpha)) / 255)
|
||||
newG := uint8(((uint32(g>>8) * invAlpha) + (uint32(cg>>8) * alpha)) / 255)
|
||||
newB := uint8(((uint32(b>>8) * invAlpha) + (uint32(cb>>8) * alpha)) / 255)
|
||||
|
||||
img.Set(px, py, color.RGBA{newR, newG, newB, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawRandomNoiseLines 绘制额外的随机干扰线
|
||||
func drawRandomNoiseLines(img *image.RGBA, rng *rand.Rand, width, height int) {
|
||||
lineCount := 8 + rng.Intn(12) // 8-19条随机线
|
||||
|
||||
for i := 0; i < lineCount; i++ {
|
||||
x1 := rng.Intn(width)
|
||||
y1 := rng.Intn(height)
|
||||
x2 := rng.Intn(width)
|
||||
y2 := rng.Intn(height)
|
||||
|
||||
col := color.RGBA{
|
||||
uint8(160 + rng.Intn(70)),
|
||||
uint8(160 + rng.Intn(70)),
|
||||
uint8(160 + rng.Intn(70)),
|
||||
uint8(150 + rng.Intn(105)),
|
||||
}
|
||||
|
||||
thickness := 1 + rng.Intn(2)
|
||||
drawThickLine(img, x1, y1, x2, y2, col, thickness)
|
||||
}
|
||||
}
|
||||
|
||||
// addBlurNoise 添加模糊噪点效果 - 增加噪点密度
|
||||
func addBlurNoise(img *image.RGBA, rng *rand.Rand) {
|
||||
// 增加随机噪点,使图片看起来更模糊(从1/50增加到1/30)
|
||||
for i := 0; i < img.Bounds().Dx()*img.Bounds().Dy()/30; i++ {
|
||||
x := rng.Intn(img.Bounds().Dx())
|
||||
y := rng.Intn(img.Bounds().Dy())
|
||||
|
||||
// 获取当前像素
|
||||
r, g, b, _ := img.At(x, y).RGBA()
|
||||
rVal := uint8(r >> 8)
|
||||
gVal := uint8(g >> 8)
|
||||
bVal := uint8(b >> 8)
|
||||
|
||||
// 添加轻微随机变化
|
||||
noise := int8(rng.Intn(21) - 10) // -10 到 10
|
||||
newR := clamp(int(rVal) + int(noise))
|
||||
newG := clamp(int(gVal) + int(noise))
|
||||
newB := clamp(int(bVal) + int(noise))
|
||||
|
||||
img.Set(x, y, color.RGBA{uint8(newR), uint8(newG), uint8(newB), 255})
|
||||
}
|
||||
}
|
||||
|
||||
func clamp(v int) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 255 {
|
||||
return 255
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// drawNumbersWithWiggle 绘制数字,带抖动效果(类似wiggle)- 增加随机分布
|
||||
func drawNumbersWithWiggle(img *image.RGBA, code string, rng *rand.Rand, width, height int, frameIndex, totalFrames int) {
|
||||
// 根据宽度和高度调整字符大小(适配新的280x50尺寸)
|
||||
charWidth := width / 18 // 约15(280宽度时为15.5)
|
||||
charHeight := height * 4 / 5 // 约40(50高度时为40)
|
||||
charSpacing := width / 25 // 字符间距,稍微减小以适应更小的空间
|
||||
|
||||
// 基础起始位置 - 更随机化
|
||||
baseStartX := width/12 + rng.Intn(width/20) // 随机起始X位置
|
||||
baseStartY := height/8 + rng.Intn(height/6) // 随机起始Y位置,允许上下移动(适配50px高度)
|
||||
|
||||
// 数字颜色(深色但稍微模糊,降低对比度)
|
||||
colors := []color.RGBA{
|
||||
{30, 30, 30, 255}, // 深灰(不是纯黑,更模糊)
|
||||
{60, 60, 60, 255}, // 中灰
|
||||
{40, 40, 80, 255}, // 深蓝(模糊)
|
||||
{80, 40, 40, 255}, // 深红(模糊)
|
||||
{50, 80, 50, 255}, // 深绿(模糊)
|
||||
{50, 50, 50, 255}, // 灰色
|
||||
}
|
||||
|
||||
// 计算抖动偏移(使用正弦波创建平滑的抖动效果)
|
||||
wiggleX := float64(frameIndex) / float64(totalFrames) * 2 * 3.14159
|
||||
wiggleY := float64(frameIndex) / float64(totalFrames) * 2 * 3.14159 * 1.3
|
||||
|
||||
for i, char := range code {
|
||||
// 随机选择颜色(偏深色但不要太黑,更模糊)
|
||||
charColor := colors[rng.Intn(len(colors))]
|
||||
|
||||
// 基础位置 - 每个字符位置增加随机偏移
|
||||
charRandomOffsetX := rng.Intn(width/25) - width/50 // ±5像素的随机偏移
|
||||
charRandomOffsetY := rng.Intn(height/8) - height/16 // ±3像素的随机偏移
|
||||
|
||||
baseX := baseStartX + i*(charWidth+charSpacing) + charRandomOffsetX
|
||||
baseY := baseStartY + charRandomOffsetY
|
||||
|
||||
// 添加抖动效果(每个字符的抖动幅度和相位不同)- 增加抖动幅度
|
||||
charWiggleX := math.Sin(wiggleX+float64(i)*0.8) * 4.0 // 从2.0增加到4.0
|
||||
charWiggleY := math.Sin(wiggleY+float64(i)*1.1) * 4.0 // 从2.0增加到4.0
|
||||
|
||||
// 额外的随机偏移 - 增加随机范围
|
||||
xOffset := int(charWiggleX) + rng.Intn(7) - 3 // 从±1增加到±3
|
||||
yOffset := int(charWiggleY) + rng.Intn(7) - 3 // 从±1增加到±3
|
||||
|
||||
x := baseX + xOffset
|
||||
y := baseY + yOffset
|
||||
|
||||
// 确保不超出边界
|
||||
if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
if x+charWidth > width {
|
||||
x = width - charWidth
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
}
|
||||
if y+charHeight > height {
|
||||
y = height - charHeight
|
||||
}
|
||||
|
||||
// 绘制数字(使用简化版本:绘制矩形块模拟数字)
|
||||
drawSimpleNumber(img, byte(char), x, y, charWidth, charHeight, charColor)
|
||||
}
|
||||
}
|
||||
|
||||
// drawSimpleNumber 绘制单个数字(简化版本,根据尺寸缩放)
|
||||
func drawSimpleNumber(img *image.RGBA, digit byte, x, y, width, height int, c color.Color) {
|
||||
digitValue := int(digit - '0')
|
||||
|
||||
// 缩放比例(相对于原始16x24大小)
|
||||
scaleX := float64(width) / 16.0
|
||||
scaleY := float64(height) / 24.0
|
||||
|
||||
// 根据数字绘制不同的小矩形块来模拟数字形状(原始坐标)
|
||||
basePatterns := [][]struct{ x, y, w, h int }{
|
||||
// 0
|
||||
{{2, 2, 14, 2}, {2, 2, 2, 20}, {14, 2, 2, 20}, {2, 20, 14, 2}},
|
||||
// 1
|
||||
{{8, 2, 2, 20}},
|
||||
// 2
|
||||
{{2, 2, 12, 2}, {12, 2, 2, 10}, {2, 10, 12, 2}, {2, 10, 2, 10}, {2, 20, 12, 2}},
|
||||
// 3
|
||||
{{2, 2, 12, 2}, {12, 2, 2, 20}, {2, 10, 10, 2}, {2, 20, 12, 2}},
|
||||
// 4
|
||||
{{2, 2, 2, 10}, {2, 10, 10, 2}, {12, 2, 2, 20}},
|
||||
// 5
|
||||
{{2, 2, 12, 2}, {2, 2, 2, 10}, {2, 10, 12, 2}, {12, 10, 2, 10}, {2, 20, 12, 2}},
|
||||
// 6
|
||||
{{2, 2, 12, 2}, {2, 2, 2, 20}, {12, 10, 2, 10}, {2, 10, 10, 2}, {2, 20, 12, 2}},
|
||||
// 7
|
||||
{{2, 2, 12, 2}, {12, 2, 2, 20}},
|
||||
// 8
|
||||
{{2, 2, 12, 2}, {2, 2, 2, 10}, {12, 2, 2, 10}, {2, 10, 12, 2}, {2, 10, 2, 10}, {12, 10, 2, 10}, {2, 20, 12, 2}},
|
||||
// 9
|
||||
{{2, 2, 12, 2}, {2, 2, 2, 10}, {12, 2, 2, 20}, {2, 10, 10, 2}},
|
||||
}
|
||||
|
||||
if digitValue >= 0 && digitValue <= 9 {
|
||||
pattern := basePatterns[digitValue]
|
||||
for _, p := range pattern {
|
||||
// 缩放坐标
|
||||
px := int(float64(p.x) * scaleX)
|
||||
py := int(float64(p.y) * scaleY)
|
||||
pw := int(float64(p.w) * scaleX)
|
||||
ph := int(float64(p.h) * scaleY)
|
||||
|
||||
// 确保最小尺寸
|
||||
if pw < 1 {
|
||||
pw = 1
|
||||
}
|
||||
if ph < 1 {
|
||||
ph = 1
|
||||
}
|
||||
|
||||
rect := image.Rect(x+px, y+py, x+px+pw, y+py+ph)
|
||||
draw.Draw(img, rect, &image.Uniform{c}, image.Point{}, draw.Over)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawAnimatedCircles 绘制移动的背景圆圈
|
||||
func drawAnimatedCircles(img *image.RGBA, data *NoiseData, frameIndex int, width, height int) {
|
||||
if frameIndex >= len(data.circles) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, circle := range data.circles[frameIndex] {
|
||||
drawCircle(img, circle.x, circle.y, circle.radius, circle.color)
|
||||
}
|
||||
}
|
||||
|
||||
// drawCircle 绘制圆圈
|
||||
func drawCircle(img *image.RGBA, cx, cy, radius int, c color.Color) {
|
||||
for y := -radius; y <= radius; y++ {
|
||||
for x := -radius; x <= radius; x++ {
|
||||
if x*x+y*y <= radius*radius {
|
||||
px := cx + x
|
||||
py := cy + y
|
||||
if px >= 0 && px < img.Bounds().Dx() && py >= 0 && py < img.Bounds().Dy() {
|
||||
// 获取当前像素并混合颜色
|
||||
r, g, b, _ := img.At(px, py).RGBA()
|
||||
cr, cg, cb, ca := c.RGBA()
|
||||
|
||||
// Alpha混合
|
||||
alpha := uint32(ca >> 8)
|
||||
invAlpha := 255 - alpha
|
||||
|
||||
newR := uint8(((uint32(r>>8) * invAlpha) + (uint32(cr>>8) * alpha)) / 255)
|
||||
newG := uint8(((uint32(g>>8) * invAlpha) + (uint32(cg>>8) * alpha)) / 255)
|
||||
newB := uint8(((uint32(b>>8) * invAlpha) + (uint32(cb>>8) * alpha)) / 255)
|
||||
|
||||
img.Set(px, py, color.RGBA{newR, newG, newB, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawAnimatedNoiseLines 绘制动态干扰线
|
||||
func drawAnimatedNoiseLines(img *image.RGBA, data *NoiseData, frameIndex int, width, height int) {
|
||||
if frameIndex >= len(data.lines) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, line := range data.lines[frameIndex] {
|
||||
drawThickLine(img, line.x1, line.y1, line.x2, line.y2, line.color, line.thickness)
|
||||
}
|
||||
}
|
||||
|
||||
// drawAnimatedNoiseDots 绘制动态干扰点
|
||||
func drawAnimatedNoiseDots(img *image.RGBA, data *NoiseData, frameIndex int, width, height int) {
|
||||
if frameIndex >= len(data.dots) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, dot := range data.dots[frameIndex] {
|
||||
if dot.color.A < 50 {
|
||||
continue // 跳过几乎透明的点
|
||||
}
|
||||
|
||||
// 绘制点(可能是1x1或2x2)
|
||||
for dx := 0; dx < dot.size && dot.x+dx < width; dx++ {
|
||||
for dy := 0; dy < dot.size && dot.y+dy < height; dy++ {
|
||||
img.Set(dot.x+dx, dot.y+dy, dot.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawThickLine 绘制粗线
|
||||
func drawThickLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color, thickness int) {
|
||||
for i := 0; i < thickness; i++ {
|
||||
offsetX := i - thickness/2
|
||||
offsetY := i - thickness/2
|
||||
drawLine(img, x1+offsetX, y1+offsetY, x2+offsetX, y2+offsetY, c)
|
||||
}
|
||||
}
|
||||
|
||||
// drawLine 绘制直线(Bresenham算法简化版)
|
||||
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) {
|
||||
dx := abs(x2 - x1)
|
||||
dy := abs(y2 - y1)
|
||||
sx := 1
|
||||
if x1 > x2 {
|
||||
sx = -1
|
||||
}
|
||||
sy := 1
|
||||
if y1 > y2 {
|
||||
sy = -1
|
||||
}
|
||||
err := dx - dy
|
||||
|
||||
x, y := x1, y1
|
||||
for {
|
||||
if x >= 0 && x < img.Bounds().Dx() && y >= 0 && y < img.Bounds().Dy() {
|
||||
img.Set(x, y, c)
|
||||
}
|
||||
if x == x2 && y == y2 {
|
||||
break
|
||||
}
|
||||
e2 := 2 * err
|
||||
if e2 > -dy {
|
||||
err -= dy
|
||||
x += sx
|
||||
}
|
||||
if e2 < dx {
|
||||
err += dx
|
||||
y += sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addNoiseDots 添加干扰点(更多更密集)
|
||||
func addNoiseDots(img *image.RGBA, rng *rand.Rand, count int) {
|
||||
for i := 0; i < count; i++ {
|
||||
x := rng.Intn(img.Bounds().Dx())
|
||||
y := rng.Intn(img.Bounds().Dy())
|
||||
|
||||
// 随机颜色(浅色,增加模糊效果)
|
||||
brightness := 140 + rng.Intn(90) // 140-230之间
|
||||
dotColor := color.RGBA{
|
||||
uint8(brightness + rng.Intn(30) - 15),
|
||||
uint8(brightness + rng.Intn(30) - 15),
|
||||
uint8(brightness + rng.Intn(30) - 15),
|
||||
255,
|
||||
}
|
||||
|
||||
// 有时绘制小区域而不是单个点(更密集的干扰)
|
||||
if rng.Intn(3) == 0 {
|
||||
// 绘制2x2的小块
|
||||
for dx := 0; dx < 2 && x+dx < img.Bounds().Dx(); dx++ {
|
||||
for dy := 0; dy < 2 && y+dy < img.Bounds().Dy(); dy++ {
|
||||
img.Set(x+dx, y+dy, dotColor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
img.Set(x, y, dotColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
Reference in New Issue
Block a user