Files
5.24BuYu/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs
T
2026-07-13 00:12:44 +08:00

228 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Text;
using UnityEngine;
/// <summary>
/// 图形验证码生成器:运行时动态生成验证码图片(Texture2D → Sprite),不依赖任何外部图片资源。
/// 用法:var sprite = CaptchaGenerator.Generate(width, height, out string code);
/// </summary>
public static class CaptchaGenerator
{
// 验证码字符集(去除易混淆字符 0/O/1/I/l)
private const string CharSet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
// 干扰线颜色池
private static readonly Color[] NoiseColors =
{
new Color(0.6f, 0.6f, 0.6f),
new Color(0.4f, 0.4f, 0.8f),
new Color(0.8f, 0.4f, 0.4f),
new Color(0.4f, 0.8f, 0.4f),
new Color(0.8f, 0.8f, 0.2f),
};
// 字符颜色池
private static readonly Color[] TextColors =
{
new Color(0.1f, 0.1f, 0.6f),
new Color(0.6f, 0.1f, 0.1f),
new Color(0.1f, 0.5f, 0.1f),
new Color(0.5f, 0.2f, 0.5f),
new Color(0.2f, 0.2f, 0.2f),
new Color(0.6f, 0.3f, 0.1f),
};
/// <summary>
/// 生成验证码图片并转换为 Sprite。
/// </summary>
/// <param name="width">图片宽度</param>
/// <param name="height">图片高度</param>
/// <param name="code">输出的验证码文本</param>
/// <param name="length">验证码字符数(默认4)</param>
/// <returns>验证码 Sprite(可直接赋给 Image)</returns>
public static Sprite Generate(int width, int height, out string code, int length = 4)
{
code = RandomCode(length);
var tex = DrawCaptcha(code, width, height);
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f));
}
/// <summary>
/// 生成验证码图片并转换为 Sprite(无 out 参数的便捷重载)。
/// </summary>
public static Sprite Generate(int width, int height, int length = 4)
{
return Generate(width, height, out _, length);
}
/// <summary>生成随机验证码文本</summary>
private static string RandomCode(int length)
{
var sb = new StringBuilder(length);
for (int i = 0; i < length; i++)
sb.Append(CharSet[Random.Range(0, CharSet.Length)]);
return sb.ToString();
}
/// <summary>把验证码画到 Texture2D 上</summary>
private static Texture2D DrawCaptcha(string code, int width, int height)
{
// 半透明背景,方便看清楚字符
var bg = new Color(0.95f, 0.95f, 0.95f, 1f);
var tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
tex.filterMode = FilterMode.Point;
var pixels = new Color32[width * height];
// 填充背景
for (int i = 0; i < pixels.Length; i++)
pixels[i] = bg;
tex.SetPixels32(pixels);
// 画干扰线
int lineCount = 3 + Random.Range(0, 3); // 3~5 条
for (int i = 0; i < lineCount; i++)
{
var c = NoiseColors[Random.Range(0, NoiseColors.Length)];
int x0 = Random.Range(0, width);
int y0 = Random.Range(0, height);
int x1 = Random.Range(0, width);
int y1 = Random.Range(0, height);
DrawLine(tex, x0, y0, x1, y1, c);
}
// 画干扰点
int dotCount = 30 + Random.Range(0, 30); // 30~59 个
for (int i = 0; i < dotCount; i++)
{
var c = NoiseColors[Random.Range(0, NoiseColors.Length)];
int x = Random.Range(0, width);
int y = Random.Range(0, height);
tex.SetPixel(x, y, c);
}
// 逐个字符绘制(使用内置 8x8 像素字体)
int charPixelSize = Mathf.Min(height * 2 / 3, width / (code.Length + 1)); // 字符像素大小
int startX = (width - code.Length * charPixelSize) / 2;
int startY = (height - charPixelSize) / 2;
for (int i = 0; i < code.Length; i++)
{
var c = TextColors[Random.Range(0, TextColors.Length)];
int x = startX + i * charPixelSize;
// 每个字符的 y 做轻微随机偏移
int y = startY + Random.Range(-charPixelSize / 6, charPixelSize / 6);
DrawChar(tex, code[i], x, y, charPixelSize, c);
}
tex.Apply();
return tex;
}
/// <summary>绘制单条直线(Bresenham)</summary>
private static void DrawLine(Texture2D tex, int x0, int y0, int x1, int y1, Color c)
{
int dx = Mathf.Abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -Mathf.Abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
while (true)
{
if (x0 >= 0 && x0 < tex.width && y0 >= 0 && y0 < tex.height)
tex.SetPixel(x0, y0, c);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
/// <summary>用 8x8 像素位图绘制单个字符,可缩放到任意像素大小</summary>
private static void DrawChar(Texture2D tex, char ch, int x, int y, int size, Color c)
{
// 5x7 像素位图(行宽5,行高7)的简单 ASCII 字库
bool[,] pattern = GetCharPattern(ch);
if (pattern == null) return;
int pw = pattern.GetLength(0); // 5
int ph = pattern.GetLength(1); // 7
float sx = size / (float)pw;
float sy = size / (float)ph;
for (int py = 0; py < ph; py++)
{
for (int px = 0; px < pw; px++)
{
if (!pattern[px, py]) continue;
// 把位图的一个像素映射到 size×size 区域(从下往上画,因为 Texture2D 原点在左下)
int xStart = Mathf.RoundToInt(x + px * sx);
int xEnd = Mathf.RoundToInt(x + (px + 1) * sx);
int yStart = Mathf.RoundToInt(y + (ph - 1 - py) * sy);
int yEnd = Mathf.RoundToInt(y + (ph - py) * sy);
for (int yy = yStart; yy < yEnd; yy++)
{
for (int xx = xStart; xx < xEnd; xx++)
{
if (xx >= 0 && xx < tex.width && yy >= 0 && yy < tex.height)
tex.SetPixel(xx, yy, c);
}
}
}
}
}
/// <summary>
/// 简单的 5x7 像素字库(支持 0-9 A-Z,去掉易混淆字符)。
/// 返回 bool[x, y],x=0..4(列),y=0..6(行,从上到下)。
/// </summary>
private static bool[,] GetCharPattern(char ch)
{
// 每个字符 7 行,每行 5 个像素(从上到下)
// '1' = 亮,'0' = 暗
string[] rows;
switch (ch)
{
case '2': rows = new[] { "01110", "10001", "00001", "00010", "00100", "01000", "11111" }; break;
case '3': rows = new[] { "01110", "10001", "00001", "00110", "00001", "10001", "01110" }; break;
case '4': rows = new[] { "00010", "00110", "01010", "10010", "11111", "00010", "00010" }; break;
case '5': rows = new[] { "11111", "10000", "11110", "00001", "00001", "10001", "01110" }; break;
case '6': rows = new[] { "00110", "01000", "10000", "11110", "10001", "10001", "01110" }; break;
case '7': rows = new[] { "11111", "00001", "00010", "00100", "01000", "01000", "01000" }; break;
case '8': rows = new[] { "01110", "10001", "10001", "01110", "10001", "10001", "01110" }; break;
case '9': rows = new[] { "01110", "10001", "10001", "01111", "00001", "00010", "01100" }; break;
case 'A': rows = new[] { "01110", "10001", "10001", "11111", "10001", "10001", "10001" }; break;
case 'B': rows = new[] { "11110", "10001", "10001", "11110", "10001", "10001", "11110" }; break;
case 'C': rows = new[] { "01110", "10001", "10000", "10000", "10000", "10001", "01110" }; break;
case 'D': rows = new[] { "11110", "10001", "10001", "10001", "10001", "10001", "11110" }; break;
case 'E': rows = new[] { "11111", "10000", "10000", "11110", "10000", "10000", "11111" }; break;
case 'F': rows = new[] { "11111", "10000", "10000", "11110", "10000", "10000", "10000" }; break;
case 'G': rows = new[] { "01110", "10001", "10000", "10111", "10001", "10001", "01110" }; break;
case 'H': rows = new[] { "10001", "10001", "10001", "11111", "10001", "10001", "10001" }; break;
case 'J': rows = new[] { "00111", "00010", "00010", "00010", "00010", "10010", "01100" }; break;
case 'K': rows = new[] { "10001", "10010", "10100", "11000", "10100", "10010", "10001" }; break;
case 'L': rows = new[] { "10000", "10000", "10000", "10000", "10000", "10000", "11111" }; break;
case 'M': rows = new[] { "10001", "11011", "10101", "10101", "10001", "10001", "10001" }; break;
case 'N': rows = new[] { "10001", "11001", "10101", "10011", "10001", "10001", "10001" }; break;
case 'P': rows = new[] { "11110", "10001", "10001", "11110", "10000", "10000", "10000" }; break;
case 'Q': rows = new[] { "01110", "10001", "10001", "10001", "10101", "10010", "01101" }; break;
case 'R': rows = new[] { "11110", "10001", "10001", "11110", "10100", "10010", "10001" }; break;
case 'S': rows = new[] { "01111", "10000", "10000", "01110", "00001", "10001", "11110" }; break;
case 'T': rows = new[] { "11111", "00100", "00100", "00100", "00100", "00100", "00100" }; break;
case 'U': rows = new[] { "10001", "10001", "10001", "10001", "10001", "10001", "01110" }; break;
case 'V': rows = new[] { "10001", "10001", "10001", "10001", "10001", "01010", "00100" }; break;
case 'W': rows = new[] { "10001", "10001", "10001", "10101", "10101", "11011", "10001" }; break;
case 'X': rows = new[] { "10001", "10001", "01010", "00100", "01010", "10001", "10001" }; break;
case 'Y': rows = new[] { "10001", "10001", "01010", "00100", "00100", "00100", "00100" }; break;
case 'Z': rows = new[] { "11111", "00001", "00010", "00100", "01000", "10000", "11111" }; break;
default: return null;
}
var pattern = new bool[5, 7];
for (int y = 0; y < 7; y++)
for (int x = 0; x < 5; x++)
pattern[x, y] = rows[y][x] == '1';
return pattern;
}
}