228 lines
10 KiB
C#
228 lines
10 KiB
C#
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;
|
||
}
|
||
}
|