LarpixServer/LarpixServer/Account/Captcha.cs
2026-04-24 07:38:15 +02:00

167 lines
No EOL
6 KiB
C#

using SkiaSharp;
using System;
using System.Runtime.InteropServices;
namespace LarpixServer.Account;
public class Captcha
{
private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static readonly SKTypeface CaptchaTypeface = SKTypeface.FromFamilyName("Google Sans Code", SKFontStyle.Bold);
public static (byte[] ImageBytes, string CaptchaText) GenerateCaptcha()
{
int width = 1120;
int height = 320;
int charCount = 8;
Span<char> captchaSpan = stackalloc char[charCount];
Span<SKColor> charColors = stackalloc SKColor[charCount];
for (int i = 0; i < charCount; i++)
{
captchaSpan[i] = Chars[Random.Shared.Next(Chars.Length)];
charColors[i] = new SKColor(
(byte)Random.Shared.Next(100, 256),
(byte)Random.Shared.Next(100, 256),
(byte)Random.Shared.Next(100, 256)
);
}
string captchaText = new string(captchaSpan);
var imageInfo = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
using var sourceBitmap = new SKBitmap(imageInfo);
using var canvas = new SKCanvas(sourceBitmap);
canvas.Clear(SKColors.Transparent);
using var dotPaint = new SKPaint { IsAntialias = true };
float spacing = 12f;
Span<float> randOffsets = stackalloc float[256];
Span<float> randRadii = stackalloc float[256];
Span<SKColor> randColors = stackalloc SKColor[256];
for (int i = 0; i < 256; i++)
{
randOffsets[i] = (float)(Random.Shared.NextDouble() - 0.5) * 8f;
randRadii[i] = (float)Random.Shared.NextDouble() * 3.5f + 1f;
randColors[i] = new SKColor(
(byte)Random.Shared.Next(120, 200),
(byte)Random.Shared.Next(120, 200),
(byte)Random.Shared.Next(120, 200)
);
}
int noiseIdx = 0;
for (float y = 0; y < height; y += spacing)
{
for (float x = 0; x < width; x += spacing)
{
noiseIdx = (noiseIdx + 1) & 255;
dotPaint.Color = randColors[noiseIdx];
canvas.DrawCircle(x + randOffsets[noiseIdx], y + randOffsets[255 - noiseIdx], randRadii[noiseIdx], dotPaint);
}
}
using var textPaint = new SKPaint
{
IsAntialias = true,
TextSize = 158,
Typeface = CaptchaTypeface
};
float cellWidth = width / (float)charCount;
for (int i = 0; i < charCount; i++)
{
string charStr = captchaText[i].ToString();
SKRect bounds = new SKRect();
textPaint.MeasureText(charStr, ref bounds);
textPaint.Color = charColors[i];
canvas.Save();
float maxRadius = (float)Math.Sqrt((bounds.Width * bounds.Width) + (bounds.Height * bounds.Height)) / 2f;
float margin = 5f;
float minX = (cellWidth * i) + maxRadius + margin;
float maxX = (cellWidth * (i + 1)) - maxRadius - margin;
float xCenter = (minX >= maxX) ? (cellWidth * i) + (cellWidth / 2f) : minX + (float)Random.Shared.NextDouble() * (maxX - minX);
float minY = maxRadius + margin;
float maxY = height - maxRadius - margin;
float yCenter = (minY >= maxY) ? height / 2f : minY + (float)Random.Shared.NextDouble() * (maxY - minY);
canvas.Translate(xCenter, yCenter);
canvas.RotateDegrees(Random.Shared.Next(-45, 46));
canvas.DrawText(charStr, -bounds.MidX, -bounds.MidY, textPaint);
canvas.Restore();
}
using var linePaint = new SKPaint { StrokeWidth = 8, IsAntialias = true };
for (int i = 0; i < charCount; i++)
{
linePaint.Color = charColors[i];
float cellStartX = cellWidth * i;
float x1 = cellStartX + (float)Random.Shared.NextDouble() * cellWidth;
float x2 = cellStartX + (float)Random.Shared.NextDouble() * cellWidth;
canvas.DrawLine(x1, 0, x2, height, linePaint);
}
float bandHeight = height / 4f;
for (int i = 0; i < 4; i++)
{
linePaint.Color = charColors[Random.Shared.Next(charColors.Length)];
float bandStartY = bandHeight * i;
float y1 = bandStartY + (float)Random.Shared.NextDouble() * bandHeight;
float y2 = bandStartY + (float)Random.Shared.NextDouble() * bandHeight;
canvas.DrawLine(0, y1, width, y2, linePaint);
}
canvas.Flush();
float amplitude = 10.5f;
float frequency = 0.03f;
Span<int> sinY = stackalloc int[height];
for (int y = 0; y < height; y++)
sinY[y] = (int)(Math.Sin(y * frequency) * amplitude);
Span<int> cosX = stackalloc int[width];
for (int x = 0; x < width; x++)
cosX[x] = (int)(Math.Cos(x * frequency) * amplitude);
using var warpedBitmap = new SKBitmap(imageInfo);
Span<uint> srcPixels = MemoryMarshal.Cast<byte, uint>(sourceBitmap.GetPixelSpan());
Span<uint> dstPixels = MemoryMarshal.Cast<byte, uint>(warpedBitmap.GetPixelSpan());
for (int y = 0; y < height; y++)
{
int offsetX = sinY[y];
int yOffsetBase = y * width;
for (int x = 0; x < width; x++)
{
int srcX = x + offsetX;
int srcY = y + cosX[x];
if ((uint)srcX < (uint)width && (uint)srcY < (uint)height)
{
dstPixels[yOffsetBase + x] = srcPixels[srcY * width + srcX];
}
else
{
dstPixels[yOffsetBase + x] = 0;
}
}
}
using var warpedImage = SKImage.FromBitmap(warpedBitmap);
using var data = warpedImage.Encode(SKEncodedImageFormat.Webp, 90);
return (data.ToArray(), captchaText);
}
}